VDD: notifications; closes #10896

The following changes have been made:
 - Assignment Created
  - students see the due date that applies to them
  - admins see "Multiple Dates"
 - Assignment Due Date Changed
  - students see the due date that applies to them;
    they receive no notification if their date doesn't change
  - admins receive a separate notification for each due
    date that changes, that they have access to;
    the message indicates which section or group applies
    (section-limited TAs will not get messages about due dates
    in sections they can't see)
 - Assignment Submitted Late
  - the message text does not change, but the student's overridden
    due date is checked
 - Group Assignment Submitted Late
  - same as previous

There were some bugs fixed along the way:
 - no longer send duplicate Assignment Submitted and
   Assignment Resubmitted notifications when an assignment
   is resubmitted
 - Group Assignment Submitted Late actually goes out
   (there was a typo in the whenever clause)

Test plan:
 - Create a course with two sections and a teacher
 - Enroll a student in each section
 - Enroll a section-limited TA in each section
 - Make sure everybody involved is signed up for "Due Date"
   notifications, ASAP
 - Using the API, Create an assignment with a default due date
   (in the past) and an overridden due date for section 2
   (in the future).  the assignment and override must be
   created in the same request (use the "Create an assignment"
   API and supply assignment[assignment_overrides]; it may
   be easier to use a JSON request body)
 - Verify that everybody got an "Assignment Created"
   message (use /users/X/messages)
   - the teacher should see "Multiple Dates",
     as should the TA in section 2 (because the default date
     is still visible to him)
   - the student and the TA in section 1 should see
     the default due date
   - the student in section 2 should see the overridden
     due date
 - "Due Date Changed" messages will not go out for assignments
   that were created less than 3 hours ago (by design, and not
   new with this changeset), so for the remaining items, you
   either need to wait 3 hours, or falsify created_at for the
   assignment you just made...
 - Change the default due date for the assignment, leaving it
   in the past
  - Everybody except the student in section 2 should get a
    notification with the new date
 - Change the overridden due date for section 2, leaving it
   in the future
  - everybody except the teacher and TA in section 1 should get
    a notification about the new date
  - the teacher and section-2 TA's notifications should indicate
    that they apply to section 2 (the student's should not)
 - submit the assignment as each student
  - the teacher should get one notification about each submission:
    the one about the student in section 1 should say it's late;
    the one about the student in section 2 should not
 - submit again
  - the teacher should get one notification about each submission:
    the one about the student in section 1 should say it's late;
    the one about the student in section 2 should not, and should
     be identified as a resubmission
     (there is no late-re-submission notification)

Change-Id: I26e57807ea0c83b69e2b532ec8822f6570ba1701
Reviewed-on: https://gerrit.instructure.com/14662
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
Jeremy Stanley 2012-10-26 14:10:08 -06:00
parent 7e2ad3caf0
commit 6edaff4a7d
33 changed files with 693 additions and 64 deletions

View File

@ -241,11 +241,17 @@ class AssignmentsApiController < ApplicationController
return if overrides && !overrides.is_a?(Array)
# do the updating
update_api_assignment(assignment, assignment_params)
assignment.transaction do
update_api_assignment(assignment, assignment_params, false)
if overrides
assignment.transaction do
assignment.save_without_broadcasting!
batch_update_assignment_overrides(assignment, overrides)
end
assignment.do_notifications!
else
assignment.save!
batch_update_assignment_overrides(assignment, overrides) if overrides
end
return true
rescue ActiveRecord::RecordInvalid
return false

View File

@ -10,7 +10,9 @@
<%= asset.title %>
<% if asset.due_at %>
<% if asset.multiple_due_dates_apply_to(user) %>
<%= t('multiple_due_dates', 'due: Multiple Dates') %>
<% elsif asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>

View File

@ -6,8 +6,10 @@
<br/><br/>
<b><a href="<%= content :link %>"><%= asset.title %></a></b>
<br/>
<% if asset.due_at %>
<% if asset.multiple_due_dates_apply_to(user) %>
<%= t('multiple_due_dates', 'due: Multiple Dates') %>
<% elsif asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>
<% end %>
<% end %>

View File

@ -1,10 +1,12 @@
<%= t('new_assignment', 'New assignment for %{course_name}', :course_name => asset.context.name) %>
<%= asset.title %>
<% if asset.due_at %>
<% if asset.multiple_due_dates_apply_to(user) %>
<%= t('multiple_due_dates', 'due: Multiple Dates') %>
<% elsif asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>
<% end %>
<%= t('more_info', 'More info at %{course_name}', :course_name => HostUrl.context_host(asset.context)) %>
<%= t('more_info', 'More info at %{course_name}', :course_name => HostUrl.context_host(asset.context)) %>

View File

@ -6,7 +6,9 @@
<%= t('assignment_created', 'Assignment Created - %{assignment_name}, %{course_name}', :assignment_name => asset.title, :course_name => asset.context.name) %>
<% end %>
<% if asset.due_at %>
<% if asset.multiple_due_dates_apply_to(user) %>
<%= t('multiple_due_dates', 'due: Multiple Dates') %>
<% elsif asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>

View File

@ -2,7 +2,9 @@
http://<%= HostUrl.context_host(asset.context) %>/<%= asset.context.class.to_s.downcase.pluralize %>/<%= asset.context_id %>/assignments/<%= asset.id %>
<% end %>
<%= t('assignment_change', 'Canvas Alert - Change: %{assignment_name}, %{course_name}', :assignment_name => asset.title, :course_name => asset.context.name) %>
<% if asset.due_at %>
<% if asset.multiple_due_dates_apply_to(user) %>
<%= t('multiple_due_dates', 'due: Multiple Dates') %>
<% elsif asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>

View File

@ -5,4 +5,4 @@
<%= t('no_due_date', 'No Due Date') %>
<% end %>
<%= t('more_info_at_url', 'More info at %{web_address}', :web_address => HostUrl.context_host(asset.context)) %>
<%= t('more_info_at_url', 'More info at %{web_address}', :web_address => HostUrl.context_host(asset.context)) %>

View File

@ -5,7 +5,7 @@
<% define_content :subject do %>
<%= t('assignment_due_date_changed', 'Assignment Due Date Changed: %{assignment_name}, %{course_name}', :assignment_name => asset.title, :course_name => asset.context.name) %>
<% end %>
<% if asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>

View File

@ -0,0 +1,19 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.assignment.context) %>/<%= asset.assignment.context.class.to_s.downcase.pluralize %>/<%= asset.assignment.context_id %>/assignments/<%= asset.assignment.id %>
<% end %>
<% define_content :subject do %>
<%= t('assignment_due_date_changed', 'Assignment Due Date Changed: %{assignment_name}, %{course_name} (%{override})', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title) %>
<% end %>
<%= t('assignment_due_date_changed_sentence', 'The due date for the assignment, %{assignment_name}, for the course, %{course_name} (%{override}), has changed to:', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title) %>
<% if asset.due_at %>
<%= datetime_string(force_zone(asset.due_at)) %>
<% else %>
<%= t('no_due_date', 'No Due Date') %>
<% end %>
<%= before_label('click_to_see_assignment', 'Click here to view the assignment') %>
<%= content :link %>

View File

@ -0,0 +1,13 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.assignment.context) %>/<%= asset.assignment.context.class.to_s.downcase.pluralize %>/<%= asset.assignment.context_id %>/assignments/<%= asset.assignment.id %>
<% end %>
<%= t('assignment_due_date_changed_sentence', 'The due date for the assignment *%{assignment_name}* for %{course_name} (%{override}) has changed to:', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title, :wrapper => "<b><a href=\"#{content :link}\">\\1</a></b>") %>
<b>
<% if asset.due_at %>
<%= datetime_string(force_zone(asset.due_at)) %>
<% else %>
<%= t('no_due_date', 'No Due Date') %>
<% end %>
</b>

View File

@ -0,0 +1,8 @@
<%= t('assignment_due_date_changed', '%{assignment_name}, %{course_name} (%{override}) is now due:', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title) %>
<% if asset.due_at %>
<%= datetime_string(force_zone(asset.due_at)) %>
<% else %>
<%= t('no_due_date', 'No Due Date') %>
<% end %>
<%= t('more_info_at_url', 'More info at %{web_address}', :web_address => HostUrl.context_host(asset.assignment.context)) %>

View File

@ -0,0 +1,13 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.assignment.context) %>/<%= asset.assignment.context.class.to_s.downcase.pluralize %>/<%= asset.assignment.context_id %>/assignments/<%= asset.assignment.id %>
<% end %>
<% define_content :subject do %>
<%= t('assignment_due_date_changed', 'Assignment Due Date Changed: %{assignment_name}, %{course_name} (%{override})', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title) %>
<% end %>
<% if asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>
<% end %>

View File

@ -0,0 +1,9 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(asset.assignment.context) %>/<%= asset.assignment.context.class.to_s.downcase.pluralize %>/<%= asset.assignment.context_id %>/assignments/<%= asset.assignment.id %>
<% end %>
<%= t('assignment_change', 'Canvas Alert - Date Change: %{assignment_name}, %{course_name} (%{override})', :assignment_name => asset.assignment.title, :course_name => asset.assignment.context.name, :override => asset.title) %>
<% if asset.due_at %>
<%= t('due_at', 'due: %{assignment_due_date_time}', :assignment_due_date_time => datetime_string(force_zone(asset.due_at))) %>
<% else %>
<%= t('no_due_date', 'due: No Due Date') %>
<% end %>

View File

@ -228,6 +228,21 @@ class Assignment < ActiveRecord::Base
:all_day_date => all_day_date }
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
end
def self.due_dates_equal?(date1, date2)
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.
@ -515,13 +530,26 @@ class Assignment < ActiveRecord::Base
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
# call this to perform notifications on an Assignment that is not being saved
# (useful when a batch of overrides associated with a new assignment have been saved)
def do_notifications!
@broadcasted = false
self.prior_version = self.versions.previous(self.current_version.number).try(:model)
self.just_created = self.prior_version.nil?
broadcast_notifications
end
set_broadcast_policy do |p|
p.dispatch :assignment_due_date_changed
p.to { participants }
p.to {
# everyone who is _not_ covered by an assignment override affecting due_at
# (the AssignmentOverride records will take care of notifying those users)
participants - participants_with_overridden_due_at
}
p.whenever { |record|
!self.suppress_broadcast and
record.context.state == :available and record.changed_in_states([:available,:published], :fields => :due_at) and
record.prior_version && (record.due_at.to_i.divmod(60)[0]) != (record.prior_version.due_at.to_i.divmod(60)[0]) and
record.prior_version && !Assignment.due_dates_equal?(record.due_at, record.prior_version.due_at) and
record.created_at < 3.hours.ago
}
@ -541,6 +569,9 @@ class Assignment < ActiveRecord::Base
!self.suppress_broadcast and
record.context.state == :available and record.just_created
}
p.filter_asset_by_recipient { |record, user|
record.overridden_for(user)
}
p.dispatch :assignment_graded
p.to { @students_whose_grade_just_changed }
@ -640,6 +671,12 @@ class Assignment < ActiveRecord::Base
self.context.participants
end
def participants_with_overridden_due_at
assignment_overrides.active.overriding_due_at.inject([]) do |overridden_users, o|
overridden_users.concat(o.applies_to_students)
end
end
def infer_state_from_course
self.workflow_state = "published" if (self.context.publish_grades_immediately rescue false)
if self.assignment_group_id.nil?
@ -1161,14 +1198,21 @@ class Assignment < ActiveRecord::Base
homework.submitted_at = Time.now
homework.with_versioning(:explicit => true) do
group ? homework.save_without_broadcast : homework.save!
if group
if student == original_student
homework.broadcast_group_submission
else
homework.save_without_broadcasting!
end
else
homework.save!
end
end
homeworks << homework
primary_homework = homework if student == original_student
end
end
end
primary_homework.broadcast_group_submission if group
homeworks.each do |homework|
context_module_action(homework.student, :submitted)
homework.add_comment({:comment => comment, :author => original_student}) if comment && (group_comment || homework == primary_homework)

View File

@ -139,6 +139,7 @@ class AssignmentOverride < ActiveRecord::Base
write_attribute(:all_day_date, new_all_day_date)
end
def as_hash
{ :title => title,
:due_at => due_at,
@ -147,6 +148,54 @@ class AssignmentOverride < ActiveRecord::Base
:override => self }
end
def applies_to_students
# FIXME: exclude students for whom this override does not apply
# because a higher-priority override exists
case set_type
when 'ADHOC'
set
when 'CourseSection'
set.participating_students
when 'Group'
set.participants
end
end
def applies_to_admins
case set_type
when 'CourseSection'
set.participating_admins
else
assignment.context.participating_admins
end
end
def notify_change?
self.assignment and
self.assignment.context.state == :available and
(self.assignment.workflow_state == 'available' || self.assignment.workflow_state == 'published') and
self.assignment.created_at < 3.hours.ago and
(!self.prior_version ||
self.workflow_state != self.prior_version.workflow_state ||
self.due_at_overridden != self.prior_version.due_at_overridden ||
self.due_at_overridden && !Assignment.due_dates_equal?(self.due_at, self.prior_version.due_at))
end
has_a_broadcast_policy
set_broadcast_policy do |p|
p.dispatch :assignment_due_date_changed
p.to { applies_to_students }
p.whenever { |record| record.notify_change? }
p.filter_asset_by_recipient { |record, user|
# note that our asset for this message is an Assignment, not an AssignmentOverride
record.assignment.overridden_for(user)
}
p.dispatch :assignment_due_date_override_changed
p.to { applies_to_admins }
p.whenever { |record| record.notify_change? }
end
named_scope :visible_to, lambda{ |admin, course|
scopes = []

View File

@ -838,15 +838,21 @@ class Attachment < ActiveRecord::Base
end
def save_without_broadcasting
@skip_broadcasts = true
save
@skip_broadcasts = false
begin
@skip_broadcasts = true
save
ensure
@skip_broadcasts = false
end
end
def save_without_broadcasting!
@skip_broadcasts = true
save!
@skip_broadcasts = false
begin
@skip_broadcasts = true
save!
ensure
@skip_broadcasts = false
end
end
# called before save

View File

@ -54,11 +54,14 @@ class CourseSection < ActiveRecord::Base
course.participating_students.scoped(:conditions => ["enrollments.course_section_id = ?", id])
end
def participants
participating_students +
def participating_admins
course.participating_admins.scoped(:conditions => ["enrollments.course_section_id = ? OR NOT COALESCE(enrollments.limit_privileges_to_course_section, ?)", id, false])
end
def participants
participating_students + participating_admins
end
def available?
course.available?
end

View File

@ -26,6 +26,7 @@ class Notification < ActiveRecord::Base
"Assignment Created",
"Assignment Changed",
"Assignment Due Date Changed",
"Assignment Due Date Override Changed",
# Submissions / Grading
"Assignment Graded",
@ -182,15 +183,20 @@ class Notification < ActiveRecord::Base
user = recipient
cc = user.email_channel
end
user_asset = asset.respond_to?(:filter_asset_by_recipient) ?
asset.filter_asset_by_recipient(self, user) : asset
next unless user_asset
I18n.locale = infer_locale(:user => user,
:context => asset.is_a?(Context) ? asset : asset.try_rescue(:context))
:context => user_asset.is_a?(Context) ? user_asset : user_asset.try_rescue(:context))
# For non-essential messages, check if too many have gone out, and if so
# send this message as a daily summary message instead of immediate.
should_summarize = user && self.summarizable? && too_many_messages?(user)
channels = CommunicationChannel.find_all_for(user, self, cc)
fallback_channel = channels.sort_by{|c| c.path_type }.first
record_delayed_messages((options || {}).merge(:user => user, :communication_channel => cc, :asset => asset, :fallback_channel => should_summarize ? channels.first : nil))
record_delayed_messages((options || {}).merge(:user => user, :communication_channel => cc, :asset => user_asset, :fallback_channel => should_summarize ? channels.first : nil))
if should_summarize
channels = channels.select{|cc| cc.path_type != 'email' && cc.path_type != 'sms' }
end
@ -212,8 +218,8 @@ class Notification < ActiveRecord::Base
message.communication_channel = c if c.is_a?(CommunicationChannel)
message.dispatch_at = nil
message.user = user
message.context = asset
message.asset_context = options[:asset_context] || asset.context(user) rescue asset
message.context = user_asset
message.asset_context = options[:asset_context] || user_asset.context(user) rescue user_asset
message.notification_category = self.category
message.delay_for = self.delay_for if self.delay_for
message.data = data if data
@ -444,6 +450,7 @@ class Notification < ActiveRecord::Base
t 'names.assignment_changed', 'Assignment Changed'
t 'names.assignment_created', 'Assignment Created'
t 'names.assignment_due_date_changed', 'Assignment Due Date Changed'
t 'names.assignment_due_date_override_changed', 'Assignment Due Date Override Changed'
t 'names.assignment_graded', 'Assignment Graded'
t 'names.assignment_resubmitted', 'Assignment Resubmitted'
t 'names.assignment_submitted', 'Assignment Submitted'

View File

@ -563,6 +563,10 @@ class Submission < ActiveRecord::Base
def <=>(other)
self.updated_at <=> other.updated_at
end
def submitted_late?
self.assignment.overridden_for(self.user).due_at <= Time.now.localtime
end
# Submission:
# Online submission submitted AFTER the due date (notify the teacher) - "Grade Changes"
@ -573,45 +577,48 @@ class Submission < ActiveRecord::Base
p.to { assignment.context.instructors_in_charge_of(user_id) }
p.whenever {|record|
!record.suppress_broadcast and
!record.group_broadcast_submission and
record.assignment.context.state == :available and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted)) and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted) || record.prior_version.try(:submitted_at) != record.submitted_at) and
record.state == :submitted and
record.has_submission? and
record.assignment.due_at <= Time.now.localtime
record.submitted_late?
}
p.dispatch :assignment_submitted
p.to { assignment.context.instructors_in_charge_of(user_id) }
p.whenever {|record|
!record.suppress_broadcast and
record.assignment.context.state == :available and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted) || record.prior_version.submitted_at != record.submitted_at) and
record.assignment.context.state == :available and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted)) and
record.state == :submitted and
record.has_submission?
record.has_submission? and
# don't send a submitted message because we already sent an :assignment_submitted_late message
!record.submitted_late?
}
p.dispatch :assignment_resubmitted
p.to { assignment.context.instructors_in_charge_of(user_id) }
p.whenever {|record|
!record.suppress_broadcast and
record.assignment.context.state == :available and
record.assignment.context.state == :available and
record.submitted? and
record.prior_version.submitted_at and
record.prior_version.submitted_at != record.submitted_at and
record.has_submission? and
# don't send a resubmitted message because we already sent a :assignment_submitted_late message.
record.assignment.due_at > Time.now.localtime
!record.submitted_late?
}
p.dispatch :group_assignment_submitted_late
p.to { assignment.context.instructors_in_charge_of(user_id) }
p.whenever {|record|
!record.suppress_broadcast and
record.group_submission_broadcast and
record.group_broadcast_submission and
record.assignment.context.state == :available and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted)) and
((record.just_created && record.submitted?) || record.changed_state_to(:submitted) || record.prior_version.try(:submitted_at) != record.submitted_at) and
record.state == :submitted and
record.assignment.due_at <= Time.now.localtime
record.submitted_late?
}
p.dispatch :submission_graded

View File

@ -0,0 +1,13 @@
class AddAssignmentDueDateOverrideNotifications < ActiveRecord::Migration
tag :predeploy
def self.up
return unless Shard.current.default?
Notification.create!(:name => "Assignment Due Date Override Changed", :category => "Due Date")
end
def self.down
return unless Shard.current.default?
Notification.find_by_name("Assignment Due Date Override Changed").try(:destroy)
end
end

View File

@ -80,13 +80,17 @@ module Api::V1::Assignment
API_ALLOWED_ASSIGNMENT_FIELDS = %w(name position points_possible grading_type due_at description)
def update_api_assignment(assignment, assignment_params)
def update_api_assignment(assignment, assignment_params, save = true)
return nil unless assignment_params.is_a?(Hash)
update_params = assignment_params.slice(*API_ALLOWED_ASSIGNMENT_FIELDS)
update_params["time_zone_edited"] = Time.zone.name if update_params["due_at"]
assignment.assignment_group = assignment.context.assignment_groups.find(assignment_params[:assignment_group_id]) if assignment_params[:assignment_group_id]
assignment.update_attributes(update_params)
if save
assignment.update_attributes(update_params)
else
assignment.attributes = update_params
end
assignment.infer_due_at
# TODO: allow rubric creation

View File

@ -256,8 +256,26 @@ namespace :db do
<%= asset.title %>, <%= asset.context.name %>, is now due:
<%= asset.due_at.strftime("%b %d at %I:%M") rescue "No Due Date" %><%= asset.due_at.strftime("%p").downcase rescue "" %>
}
create_notification 'Assignment', 'Course Content', 30*60,
create_notification 'AssignmentOverride', 'Due Date', 5*60,
'http://<%= HostUrl.context_host(asset.assignment.context) %>/<%= asset.assignment.context.class.to_s.downcase.pluralize %>/<%= asset.assignment.context_id %>/assignments/<%= asset.assignment.id %>', %{
Assignment Due Date Override Changed
Assignment Due Date Changed: <%= asset.assignment.title %>, <%= asset.assignment.context.name %> (<%= asset.title %>)
The due date for the assignment, <%= asset.assignment.title %>, for the course, <%= asset.assignment.context.name %> (<%= asset.title %>) has changed to:
<%= asset.due_at.strftime("%b %d at %I:%M") rescue "No Due Date" %><%= asset.due_at.strftime("%p").downcase rescue "" %>
Click here to view the assignment:
<%= main_link %>
}, %{
<%= asset.assignment.title %>, <%= asset.assignment.context.name %> (<%= asset.title %>, is now due:
<%= asset.due_at.strftime("%b %d at %I:%M") rescue "No Due Date" %><%= asset.due_at.strftime("%p").downcase rescue "" %>
}
create_notification 'Assignment', 'Course Content', 30*60,
'http://<%= HostUrl.context_host(asset.context) %>/<%= asset.context.class.to_s.downcase.pluralize %>/<%= asset.context_id %>/assignments/<%= asset.id %>', %{
Assignment Changed

View File

@ -212,6 +212,35 @@ describe AssignmentsApiController, :type => :integration do
@section_override.due_at.to_i.should == @section_due_at.to_i
end
it "should take overrides into account in the assignment-created notification for assignments created with overrides" do
course_with_teacher(:active_all => true)
student_in_course(:course => @course, :active_enrollment => true)
course_with_ta(:course => @course, :active_enrollment => true)
notification = Notification.create! :name => "Assignment Created"
@student.register!
@student.communication_channels.create(:path => "student@instructure.com").confirm!
@student.email_channel.notification_policies.find_or_create_by_notification_id(notification.id).update_attribute(:frequency, 'immediately')
@ta.register!
@ta.communication_channels.create(:path => "ta@instructure.com").confirm!
@ta.email_channel.notification_policies.find_or_create_by_notification_id(notification.id).update_attribute(:frequency, 'immediately')
@override_due_at = Time.parse('2002 Jun 22 12:00:00')
@user = @teacher
api_call(:post, "/api/v1/courses/#{@course.id}/assignments.json",
{ :controller => 'assignments_api', :action => 'create', :format => 'json', :course_id => @course.id.to_s },
{ :assignment => {
'name' => 'some assignment',
'assignment_overrides' => {
'0' => { 'course_section_id' => [ @course.default_section.id ], 'due_at' => @override_due_at.iso8601 }}}})
@student.messages.detect{|m| m.notification_id == notification.id}.body.should be_include 'Jun 22'
@ta.messages.detect{|m| m.notification_id == notification.id}.body.should be_include 'Multiple Dates'
end
it "should allow updating an assignment via the API" do
course_with_teacher(:active_all => true)
@start_group = @course.assignment_groups.create!({:name => "start group"})

View File

@ -37,3 +37,11 @@ def assignment_valid_attributes
:points_possible => "1.5"
}
end
def assignment_with_override(opts={})
assignment_model(opts)
@override = @a.assignment_overrides.build
@override.set = @c.default_section
@override.save!
@override
end

View File

@ -0,0 +1,28 @@
#
# Copyright (C) 2011 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_changed.twitter' do
it "should render" do
assignment_model(:title => "Quiz 1")
@object = @assignment
generate_message(:assignment_due_date_changed, :twitter, @object)
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_override_changed.email' do
it "should render" do
assignment_with_override
generate_message(:assignment_due_date_override_changed, :email, @override)
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_override_changed.facebook' do
it "should render" do
assignment_with_override
generate_message(:assignment_due_date_override_changed, :facebook, @override)
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_override_changed.sms' do
it "should render" do
assignment_with_override
generate_message(:assignment_due_date_override_changed, :sms, @override)
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_override_changed.summary' do
it "should render" do
assignment_with_override
generate_message(:assignment_due_date_override_changed, :summary, @override)
end
end

View File

@ -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 <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/messages_helper')
describe 'assignment_due_date_override_changed.twitter' do
it "should render" do
assignment_with_override
generate_message(:assignment_due_date_override_changed, :twitter, @override)
end
end

View File

@ -1862,8 +1862,150 @@ describe Assignment do
end
end
context "varied due date notifications" do
before do
course_with_teacher(:active_all => true)
@teacher.communication_channels.create(:path => "teacher@instructure.com").confirm!
@studentA = user_with_pseudonym(:active_all => true, :name => 'StudentA', :username => 'studentA@instructure.com')
@studentA.communication_channels.create(:path => "studentA@instructure.com").confirm!
@ta = user_with_pseudonym(:active_all => true, :name => 'TA1', :username => 'ta1@instructure.com')
@ta.communication_channels.create(:path => "ta1@instructure.com").confirm!
@course.enroll_student(@studentA).update_attribute(:workflow_state, 'active')
@course.enroll_user(@ta, 'TaEnrollment', :enrollment_state => 'active', :limit_privileges_to_course_section => true)
@section2 = @course.course_sections.create!(:name => 'section 2')
@studentB = user_with_pseudonym(:active_all => true, :name => 'StudentB', :username => 'studentB@instructure.com')
@studentB.communication_channels.create(:path => "studentB@instructure.com").confirm!
@ta2 = user_with_pseudonym(:active_all => true, :name => 'TA2', :username => 'ta2@instructure.com')
@ta2.communication_channels.create(:path => "ta2@instructure.com").confirm!
@section2.enroll_user(@studentB, 'StudentEnrollment', 'active')
@course.enroll_user(@ta2, 'TaEnrollment', :section => @section2, :enrollment_state => 'active', :limit_privileges_to_course_section => true)
Time.zone = 'Alaska'
default_due = DateTime.parse("01 Jan 2011 14:00 AKST")
section_2_due = DateTime.parse("02 Jan 2011 14:00 AKST")
@assignment = @course.assignments.build(:title => "some assignment", :due_at => default_due, :submission_types => ['online_text_entry'])
@assignment.save_without_broadcasting!
override = @assignment.assignment_overrides.build
override.set = @section2
override.override_due_at(section_2_due)
override.save!
end
context "assignment created" do
before do
Notification.create(:name => 'Assignment Created')
end
it "should notify of the correct due date for the recipient, or 'multiple'" do
@assignment.do_notifications!
messages_sent = @assignment.messages_sent['Assignment Created']
messages_sent.detect{|m|m.user_id == @teacher.id}.body.should be_include "Multiple Dates"
messages_sent.detect{|m|m.user_id == @studentA.id}.body.should be_include "Jan 1, 2011"
messages_sent.detect{|m|m.user_id == @ta.id}.body.should be_include "Jan 1, 2011"
messages_sent.detect{|m|m.user_id == @studentB.id}.body.should be_include "Jan 2, 2011"
messages_sent.detect{|m|m.user_id == @ta2.id}.body.should be_include "Multiple Dates"
end
it "should collapse identical instructor due dates" do
# change the override to match the default due date
override = @assignment.assignment_overrides.first
override.override_due_at(@assignment.due_at)
override.save!
@assignment.do_notifications!
# when the override matches the default, show the default and not "Multiple"
messages_sent = @assignment.messages_sent['Assignment Created']
messages_sent.each{|m| m.body.should be_include "Jan 1, 2011"}
end
end
context "assignment due date changed" do
before do
Notification.create(:name => 'Assignment Due Date Changed')
Notification.create(:name => 'Assignment Due Date Override Changed')
end
it "should notify appropriate parties when the default due date changes" do
@assignment.update_attribute(:created_at, 1.day.ago)
@assignment.due_at = DateTime.parse("09 Jan 2011 14:00 AKST")
@assignment.save!
messages_sent = @assignment.messages_sent['Assignment Due Date Changed']
messages_sent.detect{|m|m.user_id == @teacher.id}.body.should be_include "Jan 9, 2011"
messages_sent.detect{|m|m.user_id == @studentA.id}.body.should be_include "Jan 9, 2011"
messages_sent.detect{|m|m.user_id == @ta.id}.body.should be_include "Jan 9, 2011"
messages_sent.detect{|m|m.user_id == @studentB.id}.should be_nil
messages_sent.detect{|m|m.user_id == @ta2.id}.body.should be_include "Jan 9, 2011"
end
it "should notify appropriate parties when an override due date changes" do
@assignment.update_attribute(:created_at, 1.day.ago)
override = @assignment.assignment_overrides.first.reload
override.override_due_at(DateTime.parse("11 Jan 2011 11:11 AKST"))
override.save!
messages_sent = override.messages_sent['Assignment Due Date Changed']
messages_sent.detect{|m|m.user_id == @studentA.id}.should be_nil
messages_sent.detect{|m|m.user_id == @studentB.id}.body.should be_include "Jan 11, 2011"
messages_sent = override.messages_sent['Assignment Due Date Override Changed']
messages_sent.detect{|m|m.user_id == @ta.id}.should be_nil
messages_sent.detect{|m|m.user_id == @teacher.id}.body.should be_include "Jan 11, 2011"
messages_sent.detect{|m|m.user_id == @ta2.id}.body.should be_include "Jan 11, 2011"
end
end
context "assignment submitted late" do
before do
Notification.create(:name => 'Assignment Submitted')
Notification.create(:name => 'Assignment Submitted Late')
end
it "should send a late submission notification iff the submit date is late for the submitter" do
fake_submission_time = Time.parse "Jan 01 17:00:00 -0900 2011"
Time.stubs(:now).returns(fake_submission_time)
subA = @assignment.submit_homework @studentA, :submission_type => "online_text_entry", :body => "ooga"
subB = @assignment.submit_homework @studentB, :submission_type => "online_text_entry", :body => "booga"
Time.unstub(:now)
subA.messages_sent["Assignment Submitted Late"].should_not be_nil
subB.messages_sent["Assignment Submitted Late"].should be_nil
end
end
context "group assignment submitted late" do
before do
Notification.create(:name => 'Group Assignment Submitted Late')
end
it "should send a late submission notification iff the submit date is late for the group" do
@a = assignment_model(:course => @course, :group_category => "Study Groups", :due_at => Time.parse("Jan 01 17:00:00 -0900 2011"), :submission_types => ["online_text_entry"])
@group1 = @a.context.groups.create!(:name => "Study Group 1", :group_category => @a.group_category)
@group1.add_user(@studentA)
@group2 = @a.context.groups.create!(:name => "Study Group 2", :group_category => @a.group_category)
@group2.add_user(@studentB)
override = @a.assignment_overrides.new
override.set = @group2
override.override_due_at(Time.parse("Jan 03 17:00:00 -0900 2011"))
override.save!
fake_submission_time = Time.parse("Jan 02 17:00:00 -0900 2011")
Time.stubs(:now).returns(fake_submission_time)
subA = @assignment.submit_homework @studentA, :submission_type => "online_text_entry", :body => "eenie"
subB = @assignment.submit_homework @studentB, :submission_type => "online_text_entry", :body => "meenie"
Time.unstub(:now)
subA.messages_sent["Group Assignment Submitted Late"].should_not be_nil
subB.messages_sent["Group Assignment Submitted Late"].should be_nil
end
end
end
end
context "group assignment" do
it "should submit the homework for all students in the same group" do
setup_assignment_with_group

View File

@ -131,25 +131,65 @@ describe Submission do
end
context "broadcast policy" do
context "Assignment Submitted Late" do
it "should create a message when the assignment is turned in late" do
context "Submission Notifications" do
before do
Notification.create(:name => 'Assignment Submitted')
Notification.create(:name => 'Assignment Resubmitted')
Notification.create(:name => 'Assignment Submitted Late')
t = User.create(:name => "some teacher")
s = User.create(:name => "late student")
@context.enroll_teacher(t)
@context.enroll_student(s)
# @context.stubs(:teachers).returns([@user])
Notification.create(:name => 'Group Assignment Submitted Late')
@teacher = User.create(:name => "some teacher")
@student = User.create(:name => "a student")
@context.enroll_teacher(@teacher)
@context.enroll_student(@student)
end
it "should send the correct message when an assignment is turned in on-time" do
@assignment.workflow_state = "published"
@assignment.update_attributes(:due_at => Time.now + 1000)
submission_spec_model(:user => @student)
@submission.messages_sent.keys.should == ['Assignment Submitted']
end
it "should send the correct message when an assignment is turned in late" do
@assignment.workflow_state = "published"
@assignment.update_attributes(:due_at => Time.now - 1000)
# @assignment.stubs(:due_at).returns(Time.now - 100)
submission_spec_model(:user => s)
# @submission.stubs(:validate_enrollment).returns(true)
# @submission.save
@submission.messages_sent.should be_include('Assignment Submitted Late')
submission_spec_model(:user => @student)
@submission.messages_sent.keys.should == ['Assignment Submitted Late']
end
it "should send the correct message when an assignment is resubmitted on-time" do
@assignment.submission_types = ['online_text_entry']
@assignment.due_at = Time.now + 1000
@assignment.save!
@assignment.submit_homework(@student, :body => "lol")
resubmission = @assignment.submit_homework(@student, :body => "frd")
resubmission.messages_sent.keys.should == ['Assignment Resubmitted']
end
it "should send the correct message when an assignment is resubmitted late" do
@assignment.submission_types = ['online_text_entry']
@assignment.due_at = Time.now - 1000
@assignment.save!
@assignment.submit_homework(@student, :body => "lol")
resubmission = @assignment.submit_homework(@student, :body => "frd")
resubmission.messages_sent.keys.should == ['Assignment Submitted Late']
end
it "should send the correct message when a group assignment is submitted late" do
@a = assignment_model(:course => @context, :group_category => "Study Groups", :due_at => Time.now - 1000, :submission_types => ["online_text_entry"])
@group1 = @a.context.groups.create!(:name => "Study Group 1", :group_category => @a.group_category)
@group1.add_user(@student)
submission = @a.submit_homework @student, :submission_type => "online_text_entry", :body => "blah"
submission.messages_sent.keys.should == ['Group Assignment Submitted Late']
end
end
context "Submission Graded" do
it "should create a message when the assignment has been graded and published" do
Notification.create(:name => 'Submission Graded')

View File

@ -96,13 +96,21 @@ module Instructure #:nodoc:
self.current_notification.data = block
end
def filter_asset_by_recipient(&block)
self.current_notification.recipient_filter = block
end
def find_policy_for(notification)
@notifications.detect{|policy| policy.dispatch == notification.name}
end
end
class NotificationPolicy
attr_accessor :dispatch, :to, :whenever, :context, :data
attr_accessor :dispatch, :to, :whenever, :context, :data, :recipient_filter
def initialize(dispatch)
self.dispatch = dispatch
self.recipient_filter = lambda { |record, user| record }
end
# This should be called for an instance. It can only be sent out if the
@ -283,15 +291,21 @@ module Instructure #:nodoc:
attr_accessor :skip_broadcasts
def save_without_broadcasting
@skip_broadcasts = true
self.save
@skip_broadcasts = false
begin
@skip_broadcasts = true
self.save
ensure
@skip_broadcasts = false
end
end
def save_without_broadcasting!
@skip_broadcasts = true
self.save!
@skip_broadcasts = false
begin
@skip_broadcasts = true
self.save!
ensure
@skip_broadcasts = false
end
end
# The rest of the methods here should just be helper methods to make
@ -350,6 +364,10 @@ module Instructure #:nodoc:
end
alias :changed_state_to :changed_state
def filter_asset_by_recipient(notification, recipient)
policy = self.class.broadcast_policy_list.find_policy_for(notification)
policy ? policy.recipient_filter.call(self, recipient) : self
end
end # InstanceMethods
end # BroadcastPolicy