canvas-lms/lib/dates_overridable.rb

285 lines
10 KiB
Ruby
Raw Normal View History

#
# Copyright (C) 2012 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module DatesOverridable
attr_accessor :applied_overrides, :overridden_for_user, :overridden,
:has_no_overrides, :has_too_many_overrides, :preloaded_override_students
make observers viewing discussions vdd lock date aware fixes CNVS-518 also included: - observers now get visible students' section overrides in their AssignmentOverride.visible_to scope - fixed a bug where calling some DatesOverridable methods would use an overridden date where you wouldn't expect (see the specs) - added a method to get the original object from an overridden one - made DiscussionTopicPresenter handle due dates overridden to nil test plan notes - keep an eye out for regressions in displayed due dates - discussion locking behavior should be as follows -- viewing the discussion page before the earliest applicable unlock date should show a locked discussion page that lists the earliest unlock date -- viewing the discussion after the earliest unlock date should show the discussion -- the discussion page should show the due date when only one due date applies -- the discussion page should show a "multiple due dates" ui when more than one due date applies -- the "multiple due dates" ui should only display entries for sections that the observer's linked students are in -- the "multiple due dates" ui should display lock dates from the associated section override or from the original assignment if the section override does not override lock dates test plan - check an observer not observing student -- ensure that the discussion locking behavior behaves according to the observer's section's override - check an observer observing one student -- ensure that the discussion locking behaves according to the student's section's override - check an observer observing multiple student's in more than one section -- make overrides for each student's section that differ in due dates and lock dates -- ensure that the discussion locking behaves according to the students' sections' combined lock dates -- ensure that the discussion page shows a "multiple due dates" ui when the discussion is unlocked Change-Id: I8f2970f0962cdc60cf9a423f01a876bf0ae909d4 Reviewed-on: https://gerrit.instructure.com/17452 Reviewed-by: Simon Williams <simon@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Marc LeGendre <marc@instructure.com> Reviewed-by: Mark Ericksen <marke@instructure.com>
2013-02-05 08:51:39 +08:00
attr_writer :without_overrides
include DifferentiableAssignment
class NotOverriddenError < RuntimeError; end
def self.included(base)
base.has_many :assignment_overrides, :dependent => :destroy
base.has_many :active_assignment_overrides, -> { where(workflow_state: 'active') }, class_name: 'AssignmentOverride'
base.has_many :assignment_override_students, -> { where(workflow_state: 'active') }, :dependent => :destroy
base.has_many :all_assignment_override_students, class_name: 'AssignmentOverrideStudent', :dependent => :destroy
base.validates_associated :active_assignment_overrides
base.extend(ClassMethods)
end
def without_overrides
@without_overrides || self
end
def overridden_for(user, skip_clone: false)
AssignmentOverrideApplicator.assignment_overridden_for(self, user, skip_clone: skip_clone)
end
# All overrides, not just dates
def overrides_for(user, opts={})
overrides = AssignmentOverrideApplicator.overrides_for_assignment_and_user(self, user)
if opts[:ensure_set_not_empty]
overrides.select(&:set_not_empty?)
else
overrides
end
end
def overridden_for?(user)
overridden && (overridden_for_user == user)
end
def has_overrides?
if current_version?
assignment_overrides.loaded? ? assignment_overrides.any?(&:active?) : assignment_overrides.active.exists?
else
# the old version's overrides might have be deleted too but it's probably more trouble than it's worth to check here
assignment_overrides.loaded? ? assignment_overrides.any? : assignment_overrides.exists?
end
end
make fancy midnight work for assignment overrides also fixes an issue where some dates display as "Friday at 11:59pm" instead of just "Friday" Also does a little bit of refactoring and spec backfilling for the override list presenter. The override list presenter now returns a much more friendly list of "due date" hashes to the outside world to make it easier to consume in views. Views don't have to format the dates by passing in a hash anymore. test plan: - specs should pass - as a teacher, create an assignment with overrides using the web form. In one of the overrides, enter a day like March 1 at 12am. - save the overrides - Make sure fancy midnight works for lock dates and due dates, but not unlock dates (12:00 am unlock date should show up as 12:00 am, not 11:59 pm) - on the assignment's show page, you should just see "Friday", meaning that the assignment is due at 11:59 pm on March 1. - The "fancy midnight" scheme should work correctly for assignments,quizzes,and discussion topics, including the default due dates. - Be sure to check that the dates show up correctly on the assignment,quiz, and discussion show pages. - Be sure to make an override that has a blank due_at, lock_at, and unlock_at, but has a default due date, lock date, and unlock date. The overrides should not inherit from the default due date (fixes CNVS-4216) fixes CNVS-4216, CNVS-4004, CNVS-3890 Change-Id: I8b5e10c074eb2a237a1298cb7def0cb32d3dcb7f Reviewed-on: https://gerrit.instructure.com/18142 QA-Review: Amber Taniuchi <amber@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2013-03-06 00:04:59 +08:00
def has_active_overrides?
prevent assignment group movement in closed periods Assignments which have been assigned for at least one student in a closed grading period must not be moveable between assignment groups, except by admins. closes CNVS-30915 test plan: A. create or select: a. an account b. an enrollment term for the account c. a grading period set for that term with: * a closed grading period * an open grading period d. a course with that enrollment term e. two students (1 & 2) for the course f. three assignment groups (1, 2, 3) g. an assignment (A) in group 1 * due for everyone in the closed grading period h. an assignment (B) in group 1 * due for student 1 in the open grading period * due for student 2 after the open grading period i. an assignment (C) in group 2 * due for student 1 in the closed grading period * due for student 2 in the open grading period j. an assignment (D) in group 2 * due for student 1 after the open grading period * for student 2 without a due date k. an assignment (E) in group 3 * due for everyone in the open grading period B. as a Teacher in the course, visit the course assignments page a. verify assignment A cannot be moved b. verify assignment B can be moved c. verify assignment C cannot be moved d. verify assignment D can be moved e. verify assignment E can be moved C. as an Admin, visit the course assignments page a. verify assignment A can be moved b. verify assignment B can be moved c. verify assignment C can be moved d. verify assignment D can be moved e. verify assignment E can be moved Change-Id: I93a7f0f9391b493041172ed159136990c51d6a6a Reviewed-on: https://gerrit.instructure.com/91744 Tested-by: Jenkins Reviewed-by: Derek Bender <djbender@instructure.com> Reviewed-by: Neil Gupta <ngupta@instructure.com> QA-Review: Alex Morris <amorris@instructure.com> Product-Review: Christi Wruck
2016-09-17 01:54:19 +08:00
active_assignment_overrides.any?
make fancy midnight work for assignment overrides also fixes an issue where some dates display as "Friday at 11:59pm" instead of just "Friday" Also does a little bit of refactoring and spec backfilling for the override list presenter. The override list presenter now returns a much more friendly list of "due date" hashes to the outside world to make it easier to consume in views. Views don't have to format the dates by passing in a hash anymore. test plan: - specs should pass - as a teacher, create an assignment with overrides using the web form. In one of the overrides, enter a day like March 1 at 12am. - save the overrides - Make sure fancy midnight works for lock dates and due dates, but not unlock dates (12:00 am unlock date should show up as 12:00 am, not 11:59 pm) - on the assignment's show page, you should just see "Friday", meaning that the assignment is due at 11:59 pm on March 1. - The "fancy midnight" scheme should work correctly for assignments,quizzes,and discussion topics, including the default due dates. - Be sure to check that the dates show up correctly on the assignment,quiz, and discussion show pages. - Be sure to make an override that has a blank due_at, lock_at, and unlock_at, but has a default due date, lock date, and unlock date. The overrides should not inherit from the default due date (fixes CNVS-4216) fixes CNVS-4216, CNVS-4004, CNVS-3890 Change-Id: I8b5e10c074eb2a237a1298cb7def0cb32d3dcb7f Reviewed-on: https://gerrit.instructure.com/18142 QA-Review: Amber Taniuchi <amber@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2013-03-06 00:04:59 +08:00
end
def multiple_due_dates?
if overridden
!!multiple_due_dates_apply_to?(overridden_for_user)
else
raise NotOverriddenError, "#{self.class.name} has not been overridden"
end
end
def multiple_due_dates_apply_to?(user)
return false if context.user_has_been_student?(user)
if context.user_has_been_observer?(user)
observed_student_due_dates(user).length > 1
elsif context.user_has_been_admin?(user)
dates = all_dates_visible_to(user)
dates && dates.map{ |hash| self.class.due_date_compare_value(hash[:due_at]) }.uniq.size > 1
elsif context.user_has_no_enrollments?(user)
all_due_dates.length > 1
end
end
def all_due_dates
due_at_overrides = assignment_overrides.loaded? ? assignment_overrides.select{|ao| ao.active? && ao.due_at_overridden} : assignment_overrides.active.overriding_due_at
dates = due_at_overrides.map(&:as_hash)
dates << base_due_date_hash unless differentiated_assignments_applies?
dates
end
# returns a hash of observer, student, or admin to course ids.
# the observer bucket is additionally a hash with the values being a set
# of the users they observer (possibly including nil, for unassociated observers)
# note that #include?(course_id) will work equivalently on a Hash (of observers)
# or an array (of admins or students)
def self.precache_enrollments_for_multiple_assignments(assignments, user)
courses_user_has_been_enrolled_in = { observer: {}, student: [], admin: []}
current_shard = Shard.current
Shard.partition_by_shard(assignments) do |shard_assignments|
Enrollment.where(course_id: shard_assignments.map(&:context), user_id: user).
active.
distinct.
# duplicate the subquery logic of ObserverEnrollment.observed_users, where it verifies the observee exists
where("associated_user_id IS NULL OR EXISTS (
SELECT 1 FROM #{Enrollment.quoted_table_name} e2
WHERE e2.type IN ('StudentEnrollment', 'StudentViewEnrollment')
AND e2.workflow_state NOT IN ('rejected', 'completed', 'deleted', 'inactive')
AND e2.user_id=enrollments.associated_user_id
AND e2.course_id=enrollments.course_id)").
pluck(:course_id, :type, :associated_user_id).each do |(course_id, type, associated_user_id)|
relative_course_id = Shard.relative_id_for(course_id, Shard.current, current_shard)
bucket = case type
when 'ObserverEnrollment' then :observer
when 'StudentEnrollment', 'StudentViewEnrollment' then :student
# when 'TeacherEnrollment', 'TaEnrollment', 'DesignerEnrollment' then :admin
else; :admin
end
if bucket == :observer
observees = (courses_user_has_been_enrolled_in[bucket][relative_course_id] ||= Set.new)
observees << Shard.relative_id_for(associated_user_id, Shard.current, current_shard)
else
courses_user_has_been_enrolled_in[bucket] << relative_course_id
end
end
end
courses_user_has_been_enrolled_in
end
def all_dates_visible_to(user, courses_user_has_been_enrolled_in: nil)
return all_due_dates if user.nil?
if courses_user_has_been_enrolled_in
if courses_user_has_been_enrolled_in[:observer][context_id].try(:any?)
observed_student_due_dates(user, courses_user_has_been_enrolled_in[:observer][context_id].to_a)
elsif courses_user_has_been_enrolled_in[:student].include?(context_id) ||
courses_user_has_been_enrolled_in[:admin].include?(context_id) ||
courses_user_has_been_enrolled_in[:observer].include?(context_id)
overrides = overrides_for(user)
overrides = overrides.map(&:as_hash)
if !differentiated_assignments_applies? &&
(overrides.empty? || courses_user_has_been_enrolled_in[:admin].include?(context_id))
overrides << base_due_date_hash
end
overrides
else
all_due_dates
end
else
if ObserverEnrollment.observed_students(context, user).any?
observed_student_due_dates(user)
elsif context.user_has_been_student?(user) ||
context.user_has_been_admin?(user) ||
context.user_has_been_observer?(user)
overrides = overrides_for(user)
overrides = overrides.map(&:as_hash)
if !differentiated_assignments_applies? && (overrides.empty? || context.user_has_been_admin?(user))
overrides << base_due_date_hash
end
overrides
else
all_due_dates
end
end
make fancy midnight work for assignment overrides also fixes an issue where some dates display as "Friday at 11:59pm" instead of just "Friday" Also does a little bit of refactoring and spec backfilling for the override list presenter. The override list presenter now returns a much more friendly list of "due date" hashes to the outside world to make it easier to consume in views. Views don't have to format the dates by passing in a hash anymore. test plan: - specs should pass - as a teacher, create an assignment with overrides using the web form. In one of the overrides, enter a day like March 1 at 12am. - save the overrides - Make sure fancy midnight works for lock dates and due dates, but not unlock dates (12:00 am unlock date should show up as 12:00 am, not 11:59 pm) - on the assignment's show page, you should just see "Friday", meaning that the assignment is due at 11:59 pm on March 1. - The "fancy midnight" scheme should work correctly for assignments,quizzes,and discussion topics, including the default due dates. - Be sure to check that the dates show up correctly on the assignment,quiz, and discussion show pages. - Be sure to make an override that has a blank due_at, lock_at, and unlock_at, but has a default due date, lock date, and unlock date. The overrides should not inherit from the default due date (fixes CNVS-4216) fixes CNVS-4216, CNVS-4004, CNVS-3890 Change-Id: I8b5e10c074eb2a237a1298cb7def0cb32d3dcb7f Reviewed-on: https://gerrit.instructure.com/18142 QA-Review: Amber Taniuchi <amber@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2013-03-06 00:04:59 +08:00
end
def observed_student_due_dates(user, observed_student_ids = nil)
observed_students = if observed_student_ids
User.find(observed_student_ids)
else
ObserverEnrollment.observed_students(context, user).keys
end
dates = observed_students.map do |student|
self.all_dates_visible_to(student)
end
dates.flatten.uniq
end
def dates_hash_visible_to(user)
all_dates = all_dates_visible_to(user)
if all_dates
# remove base if all sections are set
overrides = all_dates.select{ |d| d[:set_type] == 'CourseSection' }
if overrides.count > 0 && overrides.count == context.active_section_count
all_dates.delete_if {|d| d[:base] }
end
formatted_dates_hash(all_dates)
else
[due_date_hash]
end
Draft State Quizzes: show multiple due dates Closes CNVS-9880 This patch enables observers watching students across multiple sections to see each section's due date for a quiz in the quizzes index page, it also fixes the dates students get to see when they're in a section other than the base one. (BREAKING?) API CHANGES ----------- --- ------- - when a student queries a quiz, the `due_at`, `lock_at`, and `unlock_at` dates they receive are that of the section they're in as oppossed to the quiz's global dates - when an observer queries a quiz, they receive the dates for the sections they're bound to in the `all_dates` field like teachers do TEST PLAN ---- ---- Two ways to test this patch: a simple way that tests only the case described by the ticket, or the comprehensive way. > The simple way - turn DS on - create a course with multiple sections - create a quiz and assign a due date to each section - as an observer who's watching more than 1 student in different sections, go to the quizzes index: - verify that you see "Multiple Dates" for due dates (and availability if you set them) - hover over the link and verify that you see the proper dates in the tooltip > The comprehensive way Check this out: https://gist.github.com/amireh/375171767da8303e1b71 Change-Id: I934cb47f0229a43713dc6b4a6d280c047a2263b9 Reviewed-on: https://gerrit.instructure.com/30083 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Myller de Araujo <myller@instructure.com> Reviewed-by: Derek DeVries <ddevries@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
2014-02-13 17:23:06 +08:00
end
def teacher_due_date_for_display(user)
ao = overridden_for user
due_at || ao.due_at || all_due_dates.dig(0, :due_at)
end
Draft State Quizzes: show multiple due dates Closes CNVS-9880 This patch enables observers watching students across multiple sections to see each section's due date for a quiz in the quizzes index page, it also fixes the dates students get to see when they're in a section other than the base one. (BREAKING?) API CHANGES ----------- --- ------- - when a student queries a quiz, the `due_at`, `lock_at`, and `unlock_at` dates they receive are that of the section they're in as oppossed to the quiz's global dates - when an observer queries a quiz, they receive the dates for the sections they're bound to in the `all_dates` field like teachers do TEST PLAN ---- ---- Two ways to test this patch: a simple way that tests only the case described by the ticket, or the comprehensive way. > The simple way - turn DS on - create a course with multiple sections - create a quiz and assign a due date to each section - as an observer who's watching more than 1 student in different sections, go to the quizzes index: - verify that you see "Multiple Dates" for due dates (and availability if you set them) - hover over the link and verify that you see the proper dates in the tooltip > The comprehensive way Check this out: https://gist.github.com/amireh/375171767da8303e1b71 Change-Id: I934cb47f0229a43713dc6b4a6d280c047a2263b9 Reviewed-on: https://gerrit.instructure.com/30083 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Myller de Araujo <myller@instructure.com> Reviewed-by: Derek DeVries <ddevries@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
2014-02-13 17:23:06 +08:00
def formatted_dates_hash(dates)
return [] if dates.blank?
Draft State Quizzes: show multiple due dates Closes CNVS-9880 This patch enables observers watching students across multiple sections to see each section's due date for a quiz in the quizzes index page, it also fixes the dates students get to see when they're in a section other than the base one. (BREAKING?) API CHANGES ----------- --- ------- - when a student queries a quiz, the `due_at`, `lock_at`, and `unlock_at` dates they receive are that of the section they're in as oppossed to the quiz's global dates - when an observer queries a quiz, they receive the dates for the sections they're bound to in the `all_dates` field like teachers do TEST PLAN ---- ---- Two ways to test this patch: a simple way that tests only the case described by the ticket, or the comprehensive way. > The simple way - turn DS on - create a course with multiple sections - create a quiz and assign a due date to each section - as an observer who's watching more than 1 student in different sections, go to the quizzes index: - verify that you see "Multiple Dates" for due dates (and availability if you set them) - hover over the link and verify that you see the proper dates in the tooltip > The comprehensive way Check this out: https://gist.github.com/amireh/375171767da8303e1b71 Change-Id: I934cb47f0229a43713dc6b4a6d280c047a2263b9 Reviewed-on: https://gerrit.instructure.com/30083 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Myller de Araujo <myller@instructure.com> Reviewed-by: Derek DeVries <ddevries@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
2014-02-13 17:23:06 +08:00
dates = dates.sort_by do |date|
due_at = date[:due_at]
[ due_at.present? ? CanvasSort::First : CanvasSort::Last, due_at.presence || CanvasSort::First ]
end
dates.map { |h| h.slice(:id, :due_at, :unlock_at, :lock_at, :title, :base) }
end
def due_date_hash
make fancy midnight work for assignment overrides also fixes an issue where some dates display as "Friday at 11:59pm" instead of just "Friday" Also does a little bit of refactoring and spec backfilling for the override list presenter. The override list presenter now returns a much more friendly list of "due date" hashes to the outside world to make it easier to consume in views. Views don't have to format the dates by passing in a hash anymore. test plan: - specs should pass - as a teacher, create an assignment with overrides using the web form. In one of the overrides, enter a day like March 1 at 12am. - save the overrides - Make sure fancy midnight works for lock dates and due dates, but not unlock dates (12:00 am unlock date should show up as 12:00 am, not 11:59 pm) - on the assignment's show page, you should just see "Friday", meaning that the assignment is due at 11:59 pm on March 1. - The "fancy midnight" scheme should work correctly for assignments,quizzes,and discussion topics, including the default due dates. - Be sure to check that the dates show up correctly on the assignment,quiz, and discussion show pages. - Be sure to make an override that has a blank due_at, lock_at, and unlock_at, but has a default due date, lock date, and unlock date. The overrides should not inherit from the default due date (fixes CNVS-4216) fixes CNVS-4216, CNVS-4004, CNVS-3890 Change-Id: I8b5e10c074eb2a237a1298cb7def0cb32d3dcb7f Reviewed-on: https://gerrit.instructure.com/18142 QA-Review: Amber Taniuchi <amber@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2013-03-06 00:04:59 +08:00
hash = { :due_at => due_at, :unlock_at => unlock_at, :lock_at => lock_at }
if self.is_a?(Assignment)
hash.merge!({ :all_day => all_day, :all_day_date => all_day_date })
make fancy midnight work for assignment overrides also fixes an issue where some dates display as "Friday at 11:59pm" instead of just "Friday" Also does a little bit of refactoring and spec backfilling for the override list presenter. The override list presenter now returns a much more friendly list of "due date" hashes to the outside world to make it easier to consume in views. Views don't have to format the dates by passing in a hash anymore. test plan: - specs should pass - as a teacher, create an assignment with overrides using the web form. In one of the overrides, enter a day like March 1 at 12am. - save the overrides - Make sure fancy midnight works for lock dates and due dates, but not unlock dates (12:00 am unlock date should show up as 12:00 am, not 11:59 pm) - on the assignment's show page, you should just see "Friday", meaning that the assignment is due at 11:59 pm on March 1. - The "fancy midnight" scheme should work correctly for assignments,quizzes,and discussion topics, including the default due dates. - Be sure to check that the dates show up correctly on the assignment,quiz, and discussion show pages. - Be sure to make an override that has a blank due_at, lock_at, and unlock_at, but has a default due date, lock date, and unlock date. The overrides should not inherit from the default due date (fixes CNVS-4216) fixes CNVS-4216, CNVS-4004, CNVS-3890 Change-Id: I8b5e10c074eb2a237a1298cb7def0cb32d3dcb7f Reviewed-on: https://gerrit.instructure.com/18142 QA-Review: Amber Taniuchi <amber@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com>
2013-03-06 00:04:59 +08:00
elsif self.assignment
hash.merge!({ :all_day => assignment.all_day, :all_day_date => assignment.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 base_due_date_hash
without_overrides.due_date_hash.merge(:base => true)
end
def context_module_tag_info(user, context, user_is_admin: false, has_submission: )
return {} unless user
self.association(:context).target ||= context
tag_info = Rails.cache.fetch_with_batched_keys(
["context_module_tag_info3", user.cache_key(:enrollments), user.cache_key(:groups)].cache_key,
batch_object: self, batched_keys: :availability
) do
hash = {}
if user_is_admin && self.has_too_many_overrides
hash[:has_many_overrides] = true
elsif self.multiple_due_dates_apply_to?(user)
hash[:vdd_tooltip] = OverrideTooltipPresenter.new(self, user).as_json
else
if due_date = self.overridden_for(user).due_at
hash[:due_date] = due_date
end
end
hash
end
tag_info[:points_possible] = self.points_possible
if user && tag_info[:due_date]
if tag_info[:due_date] < Time.now
if self.is_a?(Quizzes::Quiz) || (self.is_a?(Assignment) && expects_submission?)
tag_info[:past_due] = true unless has_submission
end
end
tag_info[:due_date] = tag_info[:due_date].utc.iso8601
end
tag_info
end
module ClassMethods
def due_date_compare_value(date)
# due dates are considered equal if they're the same up to the minute
return nil if date.nil?
date.to_i / 60
end
def due_dates_equal?(date1, date2)
due_date_compare_value(date1) == due_date_compare_value(date2)
end
end
end