From 6fc0e645814501c259a88aa441fb00a99a86cc1a Mon Sep 17 00:00:00 2001 From: Jacob Fugal Date: Tue, 2 Oct 2012 22:21:19 -0600 Subject: [PATCH] VDD: assignment override data structures refs #10831 test-plan: - run specs. this is all infrastructure, no real separate test plan. Change-Id: Ic67f574b7e4cbffd114f6ed34d306a393a6bd93c Reviewed-on: https://gerrit.instructure.com/14117 Tested-by: Jenkins Reviewed-by: Jeremy Stanley --- app/models/assignment.rb | 49 ++- app/models/assignment_override.rb | 138 +++++++ app/models/assignment_override_student.rb | 52 +++ ...001190034_assignment_override_migration.rb | 65 ++++ spec/factories/assignment_override_factory.rb | 27 ++ spec/models/assignment_override_spec.rb | 341 ++++++++++++++++++ .../assignment_override_student_spec.rb | 81 +++++ spec/models/assignment_spec.rb | 164 ++++++++- 8 files changed, 895 insertions(+), 22 deletions(-) create mode 100644 app/models/assignment_override.rb create mode 100644 app/models/assignment_override_student.rb create mode 100644 db/migrate/20121001190034_assignment_override_migration.rb create mode 100644 spec/factories/assignment_override_factory.rb create mode 100644 spec/models/assignment_override_spec.rb create mode 100644 spec/models/assignment_override_student_spec.rb diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 198a7d9f460..062f099d94c 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -50,6 +50,7 @@ class Assignment < ActiveRecord::Base belongs_to :grading_standard belongs_to :group_category has_many :assignment_reminders, :dependent => :destroy + has_many :assignment_overrides, :dependent => :destroy has_one :external_tool_tag, :class_name => 'ContentTag', :as => :context, :dependent => :destroy validates_associated :external_tool_tag, :if => :external_tool? @@ -128,7 +129,16 @@ class Assignment < ActiveRecord::Base :clear_unannounced_grading_changes_if_just_unpublished, :schedule_do_auto_peer_review_job_if_automatic_peer_review, :delete_empty_abandoned_children, - :remove_assignment_updated_flag + :remove_assignment_updated_flag, + :validate_assignment_overrides + + def validate_assignment_overrides + 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) + end + end def schedule_do_auto_peer_review_job_if_automatic_peer_review if peer_reviews && automatic_peer_reviews && !peer_reviews_assigned @@ -213,26 +223,41 @@ class Assignment < ActiveRecord::Base write_attribute :turnitin_settings, settings end + def self.all_day_interpretation(opts={}) + if opts[:due_at] + if opts[:due_at] == opts[:due_at_was] + # (comparison is modulo time zone) no real change, leave as was + return opts[:all_day_was], opts[:all_day_date_was] + else + # 'normal' case. compare due_at to fancy midnight and extract its + # date-part + return (opts[:due_at].strftime("%H:%M") == '23:59'), opts[:due_at].to_date + end + else + # no due at = all_day and all_day_date are irrelevant + return nil, nil + end + end + def default_values raise "Assignments can only be assigned to Course records" if self.context_type && self.context_type != "Course" self.context_code = "#{self.context_type.underscore}_#{self.context_id}" self.title ||= (self.assignment_group.default_assignment_name rescue nil) || "Assignment" self.grading_type = "pass_fail" if self.submission_types == "attendance" - zoned_due_at = self.due_at && ActiveSupport::TimeWithZone.new(self.due_at.utc, (ActiveSupport::TimeZone.new(self.time_zone_edited) rescue nil) || Time.zone) - if self.due_at_changed? - if zoned_due_at && zoned_due_at.strftime("%H:%M") == '23:59' - self.all_day = true - elsif self.due_at && self.due_at_was && self.all_day && self.due_at.strftime("%H:%M") == self.due_at_was.strftime("%H:%M") - self.all_day = true - else - self.all_day = false - end - end + + # make the comparison to "fancy midnight" and the date-part extraction in + # the time zone that was active during editing + time_zone = (ActiveSupport::TimeZone.new(self.time_zone_edited) rescue nil) || Time.zone + self.all_day, self.all_day_date = Assignment.all_day_interpretation( + :due_at => self.due_at ? self.due_at.in_time_zone(time_zone) : nil, + :due_at_was => self.due_at_was, + :all_day_was => self.all_day_was, + :all_day_date_was => self.all_day_date_was) + if !self.assignment_group || (self.assignment_group.deleted? && !self.deleted?) self.assignment_group = self.context.assignment_groups.active.first || self.context.assignment_groups.create! end self.mastery_score = [self.mastery_score, self.points_possible].min if self.mastery_score && self.points_possible - self.all_day_date = (zoned_due_at.to_date rescue nil) if !self.all_day_date || self.due_at_changed? || self.all_day_date_changed? self.submission_types ||= "none" self.peer_reviews_assign_at = [self.due_at, self.peer_reviews_assign_at].compact.max @workflow_state_was = self.workflow_state_was diff --git a/app/models/assignment_override.rb b/app/models/assignment_override.rb new file mode 100644 index 00000000000..0dc5bf22ded --- /dev/null +++ b/app/models/assignment_override.rb @@ -0,0 +1,138 @@ +# +# 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 . +# + +class AssignmentOverride < ActiveRecord::Base + include Workflow + include TextHelper + + simply_versioned :keep => 5 + + attr_accessible + + belongs_to :assignment + belongs_to :set, :polymorphic => true + has_many :assignment_override_students, :dependent => :destroy + + validates_presence_of :assignment, :assignment_version, :title + validates_inclusion_of :set_type, :in => %w(CourseSection Group ADHOC) + validates_length_of :title, :maximum => maximum_string_length, :allow_nil => true + + concrete_set = lambda{ |override| ['CourseSection', 'Group'].include?(override.set_type) } + + validates_presence_of :set, :set_id, :if => concrete_set + validates_uniqueness_of :set_id, :scope => [:assignment_id, :set_type, :workflow_state], :if => lambda{ |override| override.active? && concrete_set.call(override) } + validate :if => concrete_set do |record| + if record.set && record.assignment + case record.set + when CourseSection + record.errors.add :set, "not from assignment's course" unless record.set.course_id == record.assignment.context_id + when Group + record.errors.add :set, "not from assignment's group category" unless record.deleted? || record.set.group_category_id == record.assignment.group_category_id + end + end + end + + validate :set_id, :unless => concrete_set do |record| + if record.set_type == 'ADHOC' && !record.set_id.nil? + record.errors.add :set_id, "must be nil with set_type ADHOC" + end + end + + workflow do + state :active + state :deleted + end + + alias_method :destroy!, :destroy + def destroy + self.workflow_state = 'deleted' + self.save! + end + + named_scope :active, lambda { + {:conditions => {:workflow_state => 'active'} } + } + + before_validation :default_values + def default_values + self.set_type ||= 'ADHOC' + self.assignment_version = assignment.version_number if assignment + self.title = set.name if set + end + protected :default_values + + # override set read accessor and set_id read/write accessors so that reading + # set/set_id or setting set_id while set_type=ADHOC doesn't try and find the + # ADHOC model + def set_id + read_attribute(:set_id) + end + + def set_with_adhoc + if self.set_type == 'ADHOC' + nil + else + set_without_adhoc + end + end + alias_method_chain :set, :adhoc + + def set_id=(id) + if self.set_type == 'ADHOC' + write_attribute(:set_id, id) + else + super + end + end + + def self.override(field) + define_method "override_#{field}" do |value| + send("#{field}_overridden=", true) + send("#{field}=", value) + end + + define_method "clear_#{field}_override" do + send("#{field}_overridden=", false) + send("#{field}=", nil) + end + + validates_inclusion_of "#{field}_overridden", :in => [false, true] + before_validation do |override| + if override.send("#{field}_overridden").nil? + override.send("#{field}_overridden=", false) + end + true + end + end + + override :due_at + override :unlock_at + override :lock_at + + def due_at=(new_due_at) + new_all_day, new_all_day_date = Assignment.all_day_interpretation( + :due_at => new_due_at, + :due_at_was => read_attribute(:due_at), + :all_day_was => read_attribute(:all_day), + :all_day_date_was => read_attribute(:all_day_date)) + + write_attribute(:due_at, new_due_at) + write_attribute(:all_day, new_all_day) + write_attribute(:all_day_date, new_all_day_date) + end +end diff --git a/app/models/assignment_override_student.rb b/app/models/assignment_override_student.rb new file mode 100644 index 00000000000..e347b45fe26 --- /dev/null +++ b/app/models/assignment_override_student.rb @@ -0,0 +1,52 @@ +# +# 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 . +# + +class AssignmentOverrideStudent < ActiveRecord::Base + belongs_to :assignment + belongs_to :assignment_override + belongs_to :user + + attr_accessible + + validates_presence_of :assignment, :assignment_override, :user + validates_uniqueness_of :user_id, :scope => :assignment_id + + validate :assignment_override do |record| + if record.assignment_override && record.assignment_override.set_type != 'ADHOC' + record.errors.add :assignment_override, "is not adhoc" + end + end + + validate :assignment do |record| + if record.assignment_override && record.assignment_id != record.assignment_override.assignment_id + record.errors.add :assignment, "doesn't match assignment_override" + end + end + + validate :user do |record| + if record.user && record.assignment && record.user.student_enrollments.scoped(:conditions => {:course_id => record.assignment.context_id}).first.nil? + record.errors.add :user, "is not in the assignment's course" + end + end + + before_validation :default_values + def default_values + self.assignment_id = self.assignment_override.assignment_id if self.assignment_override + end + protected :default_values +end diff --git a/db/migrate/20121001190034_assignment_override_migration.rb b/db/migrate/20121001190034_assignment_override_migration.rb new file mode 100644 index 00000000000..252ba4d84e7 --- /dev/null +++ b/db/migrate/20121001190034_assignment_override_migration.rb @@ -0,0 +1,65 @@ +class AssignmentOverrideMigration < ActiveRecord::Migration + tag :predeploy + + def self.up + create_table :assignment_overrides do |t| + t.timestamps + + # generic info + t.integer :assignment_id, :null => false, :limit => 8 + t.integer :assignment_version, :null => false + t.string :set_type, :null => :false + t.integer :set_id, :limit => 8 + t.string :title + t.string :workflow_state, :null => false + + # due at override + t.boolean :due_at_overridden, :default => false, :null => false + t.datetime :due_at + t.boolean :all_day + t.date :all_day_date + + # unlock at override + t.boolean :unlock_at_overridden, :default => false, :null => false + t.datetime :unlock_at + + # lock at override + t.boolean :lock_at_overridden, :default => false, :null => false + t.datetime :lock_at + end + + if connection.adapter_name =~ /\Apostgresql/i + add_index :assignment_overrides, [:assignment_id, :set_type, :set_id], + :name => 'index_assignment_overrides_on_assignment_and_set', + :unique => true, + :conditions => "workflow_state='active' and set_id is not null" + else + # can't enforce unique without conditions, since when set_type is 'adhoc' + # and set_id null, there may be multiple overrides + add_index :assignment_overrides, [:assignment_id, :set_type, :set_id], + :name => 'index_assignment_overrides_on_assignment_and_set' + end + + add_foreign_key :assignment_overrides, :assignments + + create_table :assignment_override_students do |t| + t.timestamps + + t.integer :assignment_id, :null => false, :limit => 8 + t.integer :assignment_override_id, :null => false, :limit => 8 + t.integer :user_id, :null => false, :limit => 8 + end + + add_index :assignment_override_students, [:assignment_id, :user_id], :unique => true + add_index :assignment_override_students, :assignment_override_id + + add_foreign_key :assignment_override_students, :assignments + add_foreign_key :assignment_override_students, :assignment_overrides + add_foreign_key :assignment_override_students, :users + end + + def self.down + drop_table :assignment_override_students + drop_table :assignment_overrides + end +end diff --git a/spec/factories/assignment_override_factory.rb b/spec/factories/assignment_override_factory.rb new file mode 100644 index 00000000000..fb1b315e914 --- /dev/null +++ b/spec/factories/assignment_override_factory.rb @@ -0,0 +1,27 @@ +# +# 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 . +# + +def assignment_override_model(opts={}) + assignment = opts.delete(:assignment) || assignment_model(opts) + @override = factory_with_protected_attributes(assignment.assignment_overrides, assignment_override_valid_attributes.merge(opts)) + @override +end + +def assignment_override_valid_attributes + { :title => "Some Title" } +end diff --git a/spec/models/assignment_override_spec.rb b/spec/models/assignment_override_spec.rb new file mode 100644 index 00000000000..97d9e0b8dd9 --- /dev/null +++ b/spec/models/assignment_override_spec.rb @@ -0,0 +1,341 @@ +# +# 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 . +# + +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') + +describe AssignmentOverride do + it "should soft-delete" do + @override = assignment_override_model + @override.destroy + @override = AssignmentOverride.find_by_id(@override.id) + @override.should_not be_nil + @override.workflow_state.should == 'deleted' + end + + it "should default set_type to adhoc" do + @override = assignment_override_model + @override.valid? # trigger bookkeeping + @override.set_type.should == 'ADHOC' + end + + it "should allow reading set and set_id when set_type is adhoc" do + @override = assignment_override_model + @override.set_type = 'ADHOC' + @override.set.should be_nil + @override.set_id.should be_nil + end + + it "should be versioned" do + @override = assignment_override_model + @override.should respond_to :version_number + old_version = @override.version_number + @override.override_due_at(5.days.from_now) + @override.save! + @override.version_number.should_not == old_version + end + + it "should keep its assignment version up to date" do + @override = assignment_override_model + + @override.valid? # trigger bookkeeping + @override.assignment_version.should == @override.assignment.version_number + + old_version = @override.assignment.version_number + @override.assignment.due_at = 5.days.from_now + @override.assignment.save! + @override.assignment.version_number.should_not == old_version + + @override.valid? # trigger bookkeeping + @override.assignment_version.should == @override.assignment.version_number + end + + describe "active scope" do + it "should include active overrides" do + 5.times.map{ assignment_override_model } + AssignmentOverride.active.count.should == 5 + end + + it "should exclude deleted overrides" do + 5.times.map{ assignment_override_model.destroy } + AssignmentOverride.active.count.should == 0 + end + end + + describe "validations" do + before :each do + @override = assignment_override_model + @override.should be_valid + end + + def invalid_id_for_model(model) + (model.scoped(:select => 'max(id) as id').first.id || 0) + 1 + end + + it "should reject non-nil set_id with an adhoc set" do + @override.set_id = 1 + @override.should_not be_valid + end + + it "should reject an empty title with an adhoc set" do + @override.title = nil + @override.should_not be_valid + end + + it "should reject an empty assignment" do + @override.assignment = nil + @override.should_not be_valid + end + + it "should reject an invalid assignment" do + @override.assignment = nil + @override.assignment_id = invalid_id_for_model(Assignment) + @override.should_not be_valid + end + + it "should accept section sets" do + @override.set = @course.course_sections.create! + @override.should be_valid + end + + it "should accept group sets" do + @category = @course.group_categories.create! + @override.assignment.group_category = @category + @override.set = @category.groups.create! + @override.should be_valid + end + + it "should reject an empty set_id with a non-adhoc set_type" do + @override.set = nil + @override.set_type = 'CourseSection' + @override.set_id = nil + @override.should_not be_valid + end + + it "should reject an invalid set_id with a non-adhoc set_type" do + @override.set = nil + @override.set_type = 'CourseSection' + @override.set_id = invalid_id_for_model(CourseSection) + @override.should_not be_valid + end + + it "should reject sections in different course than assignment" do + @other_course = course_model + @override.set = @other_course.default_section + @override.should_not be_valid + end + + it "should reject groups in different category than assignment" do + @assignment.group_category = @course.group_categories.create! + @category = @course.group_categories.create! + @override.set = @category.groups.create + @override.should_not be_valid + end + + # necessary to allow deleting but otherwise keeping assignments that were + # for an assignment's previous group category when the assignment's group + # category changes + it "should not reject groups in different category than assignment when deleted" do + @assignment.group_category = @course.group_categories.create! + @category = @course.group_categories.create! + @override.set = @category.groups.create + @override.workflow_state = 'deleted' + @override.should be_valid + end + + it "should reject unrecognized sets" do + @override.set = @override.assignment.context + @override.should_not be_valid + end + + it "should reject duplicate sets" do + @override.set = @course.default_section + @override.save! + + @override = AssignmentOverride.new + @override.assignment = @assignment + @override.set = @course.default_section + @override.should_not be_valid + end + + it "should allow duplicates of sets where only one is active" do + @override.set = @course.default_section + @override.save! + @override.destroy + + @override = AssignmentOverride.new + @override.assignment = @assignment + @override.set = @course.default_section + @override.should be_valid + @override.destroy + + @override = AssignmentOverride.new + @override.assignment = @assignment + @override.set = @course.default_section + @override.should be_valid + end + end + + describe "title" do + before :each do + @override = assignment_override_model + end + + it "should force title to the name of the section" do + @section = @course.default_section + @section.name = 'Section Test Value' + @override.set = @section + @override.title = 'Other Value' + @override.valid? # trigger bookkeeping + @override.title.should == @section.name + end + + it "should default title to the name of the group" do + @assignment.group_category = @course.group_categories.create! + @group = @assignment.group_category.groups.create! + @group.name = 'Group Test Value' + @override.set = @group + @override.title = 'Other Value' + @override.valid? # trigger bookkeeping + @override.title.should == @group.name + end + + it "should not be changed for adhoc sets" do + @override.title = 'Other Value' + @override.valid? # trigger bookkeeping + @override.title.should == 'Other Value' + end + end + + def self.describe_override(field, value1, value2) + describe "#{field} overrides" do + before :each do + @assignment = assignment_model(field.to_sym => value1) + @override = assignment_override_model(:assignment => @assignment) + end + + it "should set the override when a override_#{field} is called" do + @override.send("override_#{field}", value2) + @override.send("#{field}_overridden").should == true + @override.send(field).should == value2 + end + + it "should clear the override when clear_#{field}_override is called" do + @override.send("override_#{field}", value2) + @override.send("clear_#{field}_override") + @override.send("#{field}_overridden").should == false + @override.send(field).should be_nil + end + end + end + + describe_override("due_at", 5.minutes.from_now, 7.minutes.from_now) + describe_override("unlock_at", 5.minutes.from_now, 7.minutes.from_now) + describe_override("lock_at", 5.minutes.from_now, 7.minutes.from_now) + + describe "#due_at=" do + def fancy_midnight(opts={}) + zone = opts[:zone] || Time.zone + Time.use_zone(zone) do + time = opts[:time] || Time.zone.now + time.in_time_zone.midnight + 1.day - 1.minute + end + end + + before :each do + @override = assignment_override_model + end + + it "should interpret 11:59pm as all day with no prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.all_day.should == true + end + + it "should interpret 11:59pm as all day with same-tz all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + 1.day + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.all_day.should == true + end + + it "should interpret 11:59pm as all day with other-tz all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Baghdad') + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.all_day.should == true + end + + it "should interpret 11:59pm as all day with non-all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.all_day.should == true + end + + it "should not interpret non-11:59pm as all day no prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @override.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with same-tz all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @override.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with other-tz all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Baghdad') + @override.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @override.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with non-all-day prior value" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @override.due_at = fancy_midnight(:zone => 'Alaska') + 2.hour + @override.all_day.should == false + end + + it "should preserve all-day when only changing time zone" do + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @override.all_day.should == true + end + + it "should preserve non-all-day when only changing time zone" do + @override.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @override.due_at = fancy_midnight(:zone => 'Alaska') + @override.all_day.should == false + end + + it "should determine date from due_at's timezone" do + @override.due_at = Date.today.in_time_zone('Baghdad') + 1.hour # 01:00:00 AST +03:00 today + @override.all_day_date.should == Date.today + + @override.due_at = @override.due_at.in_time_zone('Alaska') - 2.hours # 12:00:00 AKDT -08:00 previous day + @override.all_day_date.should == Date.today - 1.day + end + + it "should preserve all-day date when only changing time zone" do + @override.due_at = Date.today.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today + @override.due_at = @override.due_at.in_time_zone('Alaska') # 13:00:00 AKDT -08:00 previous day + @override.all_day_date.should == Date.today + end + + it "should preserve non-all-day date when only changing time zone" do + @override.due_at = Date.today.in_time_zone('Alaska') - 11.hours # 13:00:00 AKDT -08:00 previous day + @override.due_at = @override.due_at.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today + @override.all_day_date.should == Date.today - 1.day + end + end +end diff --git a/spec/models/assignment_override_student_spec.rb b/spec/models/assignment_override_student_spec.rb new file mode 100644 index 00000000000..7e542f4f8ae --- /dev/null +++ b/spec/models/assignment_override_student_spec.rb @@ -0,0 +1,81 @@ +# +# 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 . +# + +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') + +describe AssignmentOverrideStudent do + describe "validations" do + before :each do + student_in_course + @override = assignment_override_model(:course => @course) + @override_student = @override.assignment_override_students.build + @override_student.user = @student + end + + it "should be valid in nominal setup" do + @override_student.should be_valid + end + + it "should reject an assignment other than that of the override" do + @override_student.assignment = assignment_model + @override_student.should_not be_valid + end + + it "should reject an empty assignment_override" do + @override_student.assignment_override = nil + @override_student.should_not be_valid + end + + it "should reject a non-adhoc assignment_override" do + @override_student.assignment_override.set = @course.default_section + @override_student.should_not be_valid + end + + it "should reject an empty user" do + @override_student.user = nil + @override_student.should_not be_valid + end + + it "should reject a student not in the course" do + @override_student.user = user_model + @override_student.should_not be_valid + end + + it "should reject duplicate tuples" do + @override_student.save! + @override_student2 = @override.assignment_override_students.build + @override_student2.user = @student + @override_student2.should_not be_valid + end + end + + it "should maintain assignment from assignment_override" do + student_in_course + @override1 = assignment_override_model(:course => @course) + @override2 = assignment_override_model(:course => @course) + @override1.assignment_id.should_not == @override2.assignment_id + + @override_student = @override1.assignment_override_students.build + @override_student.user = @student + @override_student.valid? # trigger maintenance + @override_student.assignment_id.should == @override1.assignment_id + @override_student.assignment_override = @override2 + @override_student.valid? # trigger maintenance + @override_student.assignment_id.should == @override2.assignment_id + end +end diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index 255663a3e2b..f2311b06487 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -417,18 +417,162 @@ describe Assignment do end end - it "should treat 11:59pm as an all_day" do - assignment_model(:due_at => "Sep 4 2008 11:59pm") - @assignment.all_day.should eql(true) - @assignment.due_at.strftime("%H:%M").should eql("23:59") - @assignment.all_day_date.should eql(Date.parse("Sep 4 2008")) + describe "all_day and all_day_date from due_at" do + def fancy_midnight(opts={}) + zone = opts[:zone] || Time.zone + Time.use_zone(zone) do + time = opts[:time] || Time.zone.now + time.in_time_zone.midnight + 1.day - 1.minute + end + end + + before :each do + @assignment = assignment_model + end + + it "should interpret 11:59pm as all day with no prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == true + end + + it "should interpret 11:59pm as all day with same-tz all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.day + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == true + end + + it "should interpret 11:59pm as all day with other-tz all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Baghdad') + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == true + end + + it "should interpret 11:59pm as all day with non-all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == true + end + + it "should not interpret non-11:59pm as all day no prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @assignment.time_zone_edited = 'Baghdad' + @assignment.save! + @assignment.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with same-tz all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with other-tz all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Baghdad') + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == false + end + + it "should not interpret non-11:59pm as all day with non-all-day prior value" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 1.hour + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + 2.hour + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == false + end + + it "should preserve all-day when only changing time zone" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @assignment.time_zone_edited = 'Baghdad' + @assignment.save! + @assignment.all_day.should == true + end + + it "should preserve non-all-day when only changing time zone" do + @assignment.due_at = fancy_midnight(:zone => 'Alaska').in_time_zone('Baghdad') + @assignment.save! + @assignment.due_at = fancy_midnight(:zone => 'Alaska') + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day.should == false + end + + it "should determine date from due_at's timezone" do + @assignment.due_at = Date.today.in_time_zone('Baghdad') + 1.hour # 01:00:00 AST +03:00 today + @assignment.time_zone_edited = 'Baghdad' + @assignment.save! + @assignment.all_day_date.should == Date.today + + @assignment.due_at = @assignment.due_at.in_time_zone('Alaska') - 2.hours # 12:00:00 AKDT -08:00 previous day + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day_date.should == Date.today - 1.day + end + + it "should preserve all-day date when only changing time zone" do + @assignment.due_at = Date.today.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today + @assignment.time_zone_edited = 'Baghdad' + @assignment.save! + @assignment.due_at = @assignment.due_at.in_time_zone('Alaska') # 13:00:00 AKDT -08:00 previous day + @assignment.time_zone_edited = 'Alaska' + @assignment.save! + @assignment.all_day_date.should == Date.today + end + + it "should preserve non-all-day date when only changing time zone" do + @assignment.due_at = Date.today.in_time_zone('Alaska') - 11.hours # 13:00:00 AKDT -08:00 previous day + @assignment.save! + @assignment.due_at = @assignment.due_at.in_time_zone('Baghdad') # 00:00:00 AST +03:00 today + @assignment.time_zone_edited = 'Baghdad' + @assignment.save! + @assignment.all_day_date.should == Date.today - 1.day + end end - it "should not be set to all_day if a time is specified" do - assignment_model(:due_at => "Sep 4 2008 11:58pm") - @assignment.all_day.should eql(false) - @assignment.due_at.strftime("%H:%M").should eql("23:58") - @assignment.all_day_date.should eql(Date.parse("Sep 4 2008")) + it "should destroy group overrides when the group category changes" do + @assignment = assignment_model + @assignment.group_category = @assignment.context.group_categories.create! + @assignment.save! + + overrides = 5.times.map do + override = @assignment.assignment_overrides.build + override.set = @assignment.group_category.groups.create! + override.save! + + override.workflow_state.should == 'active' + override + end + + @assignment.group_category = @assignment.context.group_categories.create! + @assignment.save! + + overrides.each do |override| + override.reload + + override.workflow_state.should == 'deleted' + override.versions.size.should == 2 + override.assignment_version.should == @assignment.version_number + end end context "concurrent inserts" do