use a meaningful context for confirmation notifications

fixes #5711

also cleans up broadcast policy a bit
 * remove cruft
 * re-organize so that the DSL doesn't become methods on the model
 * add context method to the DSL that becomes asset_context on the
   message
 * only evaluate the policy block once, instead of every time a
   model that has a broadcast policy is saved

test plan:
 * add an e-mail address to a user, and check the "I want to login" checkbox
 * the notification e-mail should link back to the correct domain

Change-Id: I484d07963c86c81f9b0226fd942ef9a71ac5500c
Reviewed-on: https://gerrit.instructure.com/8830
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
Reviewed-by: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Cody Cutrer 2012-02-20 13:33:49 -07:00
parent dadfce7c87
commit b48b122438
19 changed files with 362 additions and 752 deletions

View File

@ -37,7 +37,7 @@ class CommunicationChannelsController < ApplicationController
@cc.workflow_state = 'unconfirmed'
@cc.build_pseudonym_on_confirm = params[:build_pseudonym] == '1'
if @cc.save
@cc.send_confirmation!
@cc.send_confirmation!(@domain_root_account)
flash[:notice] = "Contact method registered!"
render :json => @cc.to_json(:only => [:id, :user_id, :path, :path_type])
else
@ -172,7 +172,7 @@ class CommunicationChannelsController < ApplicationController
new_cc ||= @user.communication_channels.build(:path => @pseudonym.unique_id)
new_cc.user = @user
new_cc.workflow_state = 'unconfirmed' if new_cc.retired?
new_cc.send_confirmation! if new_cc.unconfirmed?
new_cc.send_confirmation!(@root_account) if new_cc.unconfirmed?
new_cc.save! if new_cc.changed?
@pseudonym.communication_channel = new_cc
end
@ -220,7 +220,7 @@ class CommunicationChannelsController < ApplicationController
@enrollment.re_send_confirmation!
else
@cc = @user.communication_channels.find(params[:id])
@cc.send_confirmation!
@cc.send_confirmation!(@domain_root_account)
end
render :json => {:re_sent => true}
end

View File

@ -1,5 +1,5 @@
<% define_content :link do %>
http://<%= HostUrl.context_host(((asset.pseudonym || asset.user.pseudonym).account rescue nil)) %>/register/<%= asset.confirmation_code %>
http://<%= HostUrl.context_host(asset_context) || HostUrl.default_host %>/register/<%= asset.confirmation_code %>
<% end %>
<% define_content :subject do %>
@ -9,7 +9,7 @@
<%= t :body,
"The email address, %{email} is being registered at %{website} for the user, %{user}.",
:email => asset.path,
:website => (HostUrl.context_host((asset.pseudonym || asset.user.pseudonym).account) rescue nil) || HostUrl.default_host,
:website => HostUrl.context_host(asset_context) || HostUrl.default_host,
:user => asset.user.name %>
<%= t :details, "To confirm this registration, please visit the following url:" %>

View File

@ -1,4 +1,4 @@
<%= t :body, <<-BODY, :confirmation_code => asset.confirmation_code, :user => asset.user.name, :website => (HostUrl.context_host((asset.pseudonym || asset.user.pseudonym).account) rescue nil) || HostUrl.default_host
<%= t :body, <<-BODY, :confirmation_code => asset.confirmation_code, :user => asset.user.name, :website => HostUrl.context_host(asset_context) || HostUrl.default_host
Confirm Canvas registration w/ code:
%{confirmation_code}

View File

@ -70,6 +70,7 @@ class CommunicationChannel < ActiveRecord::Base
record.workflow_state == 'unconfirmed' and self.user.registered? and
self.path_type == 'email'
}
p.context { @root_account }
p.dispatch :merge_email_communication_channel
p.to { self }
@ -86,8 +87,9 @@ class CommunicationChannel < ActiveRecord::Base
self.path_type == 'sms' and
!self.user.creation_pending?
}
p.context { @root_account }
end
def active_pseudonyms
self.user.pseudonyms.active
end
@ -128,9 +130,11 @@ class CommunicationChannel < ActiveRecord::Base
@request_password = false
end
def send_confirmation!
def send_confirmation!(root_account)
@send_confirmation = true
@root_account = root_account
self.save!
@root_account = nil
@send_confirmation = false
end

View File

@ -20,7 +20,8 @@ class DelayedNotification < ActiveRecord::Base
include Workflow
belongs_to :asset, :polymorphic => true
belongs_to :notification
attr_accessible :asset, :notification, :recipient_keys
belongs_to :asset_context, :polymorphic => true
attr_accessible :asset, :notification, :recipient_keys, :asset_context
serialize :recipient_keys
@ -32,14 +33,15 @@ class DelayedNotification < ActiveRecord::Base
state :errored
end
def self.process(asset, notification, recipient_keys)
dn = DelayedNotification.new(:asset => asset, :notification => notification, :recipient_keys => recipient_keys)
def self.process(asset, notification, recipient_keys, asset_context)
dn = DelayedNotification.new(:asset => asset, :notification => notification, :recipient_keys => recipient_keys,
:asset_context => asset_context)
dn.process
end
def process
tos = self.to_list
res = self.notification.create_message(self.asset, tos) if self.asset && !tos.empty?
res = self.notification.create_message(self.asset, tos, :asset_context => self.asset_context) if self.asset && !tos.empty?
self.do_process unless self.new_record?
res
rescue => e

View File

@ -196,7 +196,7 @@ class Notification < ActiveRecord::Base
:subject => self.subject,
:to => to_path
)
message.body = self.body
message.body = self.sms_body if c.respond_to?("path_type") && c.path_type == "sms"
message.notification_name = self.name

View File

@ -0,0 +1,13 @@
class AddAssetContextToDelayedNotification < ActiveRecord::Migration
tag :predeploy
def self.up
add_column :delayed_notifications, :asset_context_type, :string
add_column :delayed_notifications, :asset_context_id, :integer, :limit => 8
end
def self.down
remove_column :delayed_notifications, :asset_context_id
remove_column :delayed_notifications, :asset_context_type
end
end

View File

@ -25,12 +25,6 @@ describe Announcement do
end
context "broadcast policy" do
it "should have a broadcast policy" do
announcement_model
@a.should be_respond_to(:dispatch)
@a.should be_respond_to(:to)
end
it "should sanitize message" do
announcement_model
@a.message = "<a href='#' onclick='alert(12);'>only this should stay</a>"

View File

@ -35,17 +35,6 @@ describe AssignmentGroup do
end
context "broadcast policy" do
it "should have a broadcast policy" do
assignment_group_model
@ag.should be_respond_to(:dispatch)
@ag.should be_respond_to(:to)
end
# it "should have 1 policy defined" do
# assignment_group_model
# @ag.broadcast_policy_list.size.should eql(1)
# end
context "grade weight changed" do
# it "should have a 'Grade Weight Changed' policy" do
# assignment_group_model

View File

@ -1028,12 +1028,6 @@ describe Assignment do
end
context "broadcast policy" do
it "should have a broadcast policy" do
assignment_model
@a.should be_respond_to(:dispatch)
@a.should be_respond_to(:to)
end
context "due date changed" do
it "should create a message when an assignment due date has changed" do
Notification.create(:name => 'Assignment Due Date Changed')

View File

@ -311,4 +311,20 @@ describe CommunicationChannel do
# the unconfirmed should still be valid, even though a retired exists
@cc.should be_valid
end
context "notifications" do
it "should forward the root account to the message" do
notification = Notification.create!(:name => 'Confirm Email Communication Channel', :category => 'Registration')
@user = User.create!
@user.register!
@cc = @user.communication_channels.create!(:path => 'user1@example.com')
account = Account.create!
HostUrl.stubs(:context_host).with(account).returns('someserver.com')
HostUrl.stubs(:context_host).with(nil).returns('default')
@cc.send_confirmation!(account)
message = Message.find(:first, :conditions => { :communication_channel_id => @cc.id, :notification_id => notification.id })
message.should_not be_nil
message.body.should match /someserver.com/
end
end
end

View File

@ -117,12 +117,6 @@ describe Submission do
end
context "broadcast policy" do
it "should have a broadcast policy" do
submission_spec_model
@submission.should be_respond_to(:dispatch)
@submission.should be_respond_to(:to)
end
context "Assignment Submitted Late" do
it "should create a message when the assignment is turned in late" do
Notification.create(:name => 'Assignment Submitted Late')

View File

@ -1,27 +0,0 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run specs.'
task :default => :spec
desc 'Run specs'
task :spec do
`spec spec/`
end
desc 'Test the adheres_to_policy plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the adheres_to_policy plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AdheresToPolicy'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@ -1,2 +1,2 @@
require 'broadcast_policy'
ActiveRecord::Base.send :extend, Instructure::Broadcast::Policy::ClassMethods
ActiveRecord::Base.send :extend, Instructure::BroadcastPolicy::ClassMethods

View File

@ -16,323 +16,331 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This should work like this:
#
# class Account < ActiveRecord::Base
# has_a_broadcast_policy
#
# set_broadcast_policy do
# dispatch(:name)
# to { some_list }
# whenever { |obj| obj.something == condition }
# end
# end
#
# Some useful examples:
#
# set_broadcast_policy do
# dispatch :new_assignment
# to { self.students }
# whenever { |record| record.just_created? }
# end
#
# set_broadcast_policy do
# dispatch :assignment_change
# to { self.students }
# whenever { |record|
# record.prior_version != self.version and true
# # ... some field-wise comparison
# }
# end
#
# u = User.find(:first)
# a = Account.find(:first)
# a.check_policy(u)
module Instructure #:nodoc:
module Broadcast #:nodoc:
# This should work like this:
#
# class Account < ActiveRecord::Base
# has_a_broadcast_policy
#
# set_broadcast_policy do
# dispatch(:name)
# to { some_list }
# whenever { |obj| obj.something == condition }
# end
# end
#
# Some useful examples:
#
# set_broadcast_policy do
# dispatch :new_assignment
# to { self.students }
# whenever { |record| record.just_created? }
# end
#
# set_broadcast_policy do
# dispatch :assignment_change
# to { self.students }
# whenever { |record|
# record.prior_version != self.version and true
# # ... some field-wise comparison
# }
# end
#
# u = User.find(:first)
# a = Account.find(:first)
# a.check_policy(u)
module Policy
class PolicyStorage
attr_accessor :dispatch, :to, :whenever
def initialize(dispatch)
self.dispatch = dispatch
end
# This should be called for an instance. It can only be sent out if the
# condition is met, if there is a notification that we can find, and if
# there is someone to send this to. At this point, a Message record is
# created, which will be delayed, consolidated, dispatched to the right
# server, and then finally sent through that server.
#
# This now sets a series of temporary flags while working for audit
# reasons.
def broadcast(record)
if (record.skip_broadcasts rescue false)
record.messages_failed[self.dispatch] = "Broadcasting explicitly skipped"
return false
end
begin
meets_condition = self.whenever.call(record)
rescue
meets_condition = false
record.messages_failed[self.dispatch] = "Error thrown attempting to meet condition."
return false
end
module BroadcastPolicy #:nodoc:
unless meets_condition
record.messages_failed[self.dispatch] = "Did not meet condition."
return false
end
notification = record.notifications.find_by_name(self.dispatch) rescue nil
notification ||= Notification.find_by_name(self.dispatch)
# logger.warn "Could not find notification for #{record.inspect}" unless notification
unless notification
record.messages_failed[self.dispatch] = "Could not find notification: #{self.dispatch}."
return false
end
# self.consolidated_notifications[notification_name.to_s.titleize] rescue nil
begin
to_list = self.to.call(record)
rescue
to_list = nil
record.messages_failed[self.dispatch] = "Error thrown attempting to generate a recipient list."
return false
end
unless to_list
record.messages_failed[self.dispatch] = "Could not generate a recipient list."
return false
end
to_list = Array[to_list].flatten
n = DelayedNotification.send_later_if_production_enqueue_args(
:process,
{ :priority => Delayed::LOW_PRIORITY },
record, notification, (to_list || []).compact.map(&:asset_string))
n ||= DelayedNotification.new(:asset => record, :notification => notification, :recipient_keys => (to_list || []).compact.map(&:asset_string))
if Rails.env.test?
record.messages_sent[self.dispatch] = n.is_a?(DelayedNotification) ? n.process : n
end
n
# notification.create_message(record, to_list)
end
end # PolicyStorage
module ClassMethods #:nodoc:
def has_a_broadcast_policy
extend Instructure::Broadcast::Policy::SingletonMethods
include Instructure::Broadcast::Policy::InstanceMethods
after_save :broadcast_notifications # Must be defined locally...
before_save :set_broadcast_flags
end
# Uses the 'context' relationship as the governing relationship.
# Canonically, this probably will look something like:
# course -> professor -> section -> department -> account.
# So, this method recurses the list, keeping the nearer values. So,
# account can setup a series of default notifications, but a professor
# can override these.
# Removing for now, until we memoize this
# def consolidated_notifications
# instance_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, self.id)
# class_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, nil)
# context_notifications = context.consolidated_notifications if
# defined?(context) and context.respond_to?(:consolidated_notifications)
# context_notifications ||= []
#
# cn = hashify(*instance_notifications)
# cn.reverse_merge!(*class_notifications)
# cn.reverse_merge!(*context_notifications)
# cn
# end
#
# def hashify(*list)
# list.inject({}) {|h, v| h[v.name] = v; h}
# end
# protected :hashify
class PolicyList
def initialize
@notifications = []
end
# This is where the DSL is defined.
module SingletonMethods
def self.extended(klass)
klass.send(:class_inheritable_accessor, :broadcast_policy_block)
end
# This stores the policy for broadcasting changes on a class. It works like a
# macro. The policy block will be stored in @broadcast_policy_block. Then, an
# instance will use that to instantiate a Policy object.
def set_broadcast_policy(&block)
self.broadcast_policy_block = block
end
end # SingletonMethods
module InstanceMethods
# Some generic flags for inside the policy
attr_accessor :just_created, :prior_version
# Some flags for auditing policy matching
def messages_sent
@messages_sent ||= {}
def populate(&block)
self.instance_eval(&block)
@current_notification = nil
end
def broadcast(record)
@notifications.each { |notification| notification.broadcast(record) }
end
def dispatch(notification_name)
titleized = notification_name.to_s.titleize.gsub(/sms/i, "SMS")
@current_notification = @notifications.find { |notification| notification.dispatch == titleized }
return if @current_notification
@current_notification = NotificationPolicy.new(titleized)
@notifications << @current_notification
end
def current_notification
raise "Must call dispatch in the policy block first" unless @current_notification
@current_notification
end
protected :current_notification
def to(&block)
self.current_notification.to = block
end
def whenever(&block)
self.current_notification.whenever = block
end
def context(&block)
self.current_notification.context = block
end
end
class NotificationPolicy
attr_accessor :dispatch, :to, :whenever, :context
def initialize(dispatch)
self.dispatch = dispatch
end
# This should be called for an instance. It can only be sent out if the
# condition is met, if there is a notification that we can find, and if
# there is someone to send this to. At this point, a Message record is
# created, which will be delayed, consolidated, dispatched to the right
# server, and then finally sent through that server.
#
# This now sets a series of temporary flags while working for audit
# reasons.
def broadcast(record)
if (record.skip_broadcasts rescue false)
record.messages_failed[self.dispatch] = "Broadcasting explicitly skipped"
return false
end
def clear_broadcast_messages
@messages_sent = {}
@messages_failed = {}
end
# Whenever a requirement fails, this is stored here.
def messages_failed
@messages_failed ||= {}
begin
meets_condition = record.instance_eval &self.whenever
rescue
meets_condition = false
record.messages_failed[self.dispatch] = "Error thrown attempting to meet condition."
return false
end
# This is called before_save
def set_broadcast_flags
@broadcasted = false
unless @skip_broadcasts
self.just_created = self.new_record?
self.prior_version = generate_prior_version
end
unless meets_condition
record.messages_failed[self.dispatch] = "Did not meet condition."
return false
end
def generate_prior_version
obj = self.class.new
self.attributes.each do |attr, value|
obj.__send__("#{attr}=", value) rescue nil
end
self.changes.each do |attr, values|
obj.__send__("#{attr}=", values[0]) rescue nil
end
obj.workflow_state = self.workflow_state_was if obj.respond_to?("workflow_state=") && self.respond_to?("workflow_state_was")
obj
notification = record.notifications.find_by_name(self.dispatch) rescue nil
notification ||= Notification.find_by_name(self.dispatch)
# logger.warn "Could not find notification for #{record.inspect}" unless notification
unless notification
record.messages_failed[self.dispatch] = "Could not find notification: #{self.dispatch}."
return false
end
# This is called after_save
def broadcast_notifications
return if @broadcasted
@broadcasted = true
raise ArgumentError, "Broadcast Policy block not supplied for #{self.class.to_s}" unless self.class.broadcast_policy_block
# our common pattern is to do:
# set_broadcast_policy do |p|
# ...
# end
# note that p is really just self, in that block
self.instance_eval &self.class.broadcast_policy_block
self.broadcast_policy_list.each {|p| p.broadcast(self) }
self.broadcast_policy_list.clear
# self.consolidated_notifications[notification_name.to_s.titleize] rescue nil
begin
to_list = record.instance_eval &self.to
rescue
to_list = nil
record.messages_failed[self.dispatch] = "Error thrown attempting to generate a recipient list."
return false
end
unless to_list
record.messages_failed[self.dispatch] = "Could not generate a recipient list."
return false
end
to_list = Array[to_list].flatten
begin
asset_context = record.instance_eval &self.context if self.context
rescue
record.messages_failed[self.dispatch] = "Error thrown attempting to get asset_context."
return false
end
def broadcast_policy_list
@broadcast_policy_list ||= []
n = DelayedNotification.send_later_if_production_enqueue_args(
:process,
{ :priority => Delayed::LOW_PRIORITY },
record, notification, (to_list || []).compact.map(&:asset_string), asset_context)
n ||= DelayedNotification.new(:asset => record, :notification => notification,
:recipient_keys => (to_list || []).compact.map(&:asset_string),
:asset_context => asset_context)
if Rails.env.test?
record.messages_sent[self.dispatch] = n.is_a?(DelayedNotification) ? n.process : n
end
n
# notification.create_message(record, to_list)
end
end # NotificationPolicy
# If this is nil, we don't worry about trying to implement anything.
def dispatch(notification_name)
found = self.broadcast_policy_list.find {|bp| bp.dispatch == titleized(notification_name)}
return found if found
self.broadcast_policy_list << PolicyStorage.new(titleized(notification_name))
end
def titleized(notification_name)
notification_name.to_s.titleize.gsub(/sms/i, "SMS")
end
protected :titleized
def implementing_policy
if not self.broadcast_policy_list.last
# This really shouldn't happen, if a policy is setup right, but it
# should be logged and silently fail.
self.broadcast_policy_list << PolicyStorage.new("unknown")
end
self.broadcast_policy_list.last
end
def to(&block)
self.implementing_policy.to = block
end
def whenever(&block)
self.implementing_policy.whenever = block
end
attr_accessor :skip_broadcasts
def save_without_broadcasting
@skip_broadcasts = true
self.save
@skip_broadcasts = false
end
def save_without_broadcasting!
@skip_broadcasts = true
self.save!
@skip_broadcasts = false
end
# The rest of the methods here should just be helper methods to make
# writing a condition that much easier.
def changed_in_state(state, opts={})
fields = opts[:fields] || []
fields = [fields] unless fields.is_a?(Array)
module ClassMethods #:nodoc:
def has_a_broadcast_policy
extend Instructure::BroadcastPolicy::SingletonMethods
include Instructure::BroadcastPolicy::InstanceMethods
after_save :broadcast_notifications # Must be defined locally...
before_save :set_broadcast_flags
end
# Come back to this to debug some of the notifications
# if fields == [:due_at]
# require 'rubygems'
# require 'ruby-debug'
# debugger
# 1 + 1
# end
begin
fields.map {|field| self.prior_version.send(field) != self.send(field) }.include?(true) and
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a change was made: #{e.inspect}"
false
end
end
def changed_in_states(states, opts={})
!states.select{|s| changed_in_state(s, opts)}.empty?
end
# Uses the 'context' relationship as the governing relationship.
# Canonically, this probably will look something like:
# course -> professor -> section -> department -> account.
# So, this method recurses the list, keeping the nearer values. So,
# account can setup a series of default notifications, but a professor
# can override these.
def remained_in_state(state)
begin
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a record remained in the same state: #{e.inspect}"
false
end
# Removing for now, until we memoize this
# def consolidated_notifications
# instance_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, self.id)
# class_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, nil)
# context_notifications = context.consolidated_notifications if
# defined?(context) and context.respond_to?(:consolidated_notifications)
# context_notifications ||= []
#
# cn = hashify(*instance_notifications)
# cn.reverse_merge!(*class_notifications)
# cn.reverse_merge!(*context_notifications)
# cn
# end
#
# def hashify(*list)
# list.inject({}) {|h, v| h[v.name] = v; h}
# end
# protected :hashify
end
# This is where the DSL is defined.
module SingletonMethods
def self.extended(klass)
klass.send(:class_inheritable_accessor, :broadcast_policy_list)
end
# This stores the policy for broadcasting changes on a class. It works like a
# macro. The policy block will be stored in @broadcast_policy.
def set_broadcast_policy(&block)
self.broadcast_policy_list = PolicyList.new
self.broadcast_policy_list.populate(&block)
end
end # SingletonMethods
module InstanceMethods
# Some generic flags for inside the policy
attr_accessor :just_created, :prior_version
# Some flags for auditing policy matching
def messages_sent
@messages_sent ||= {}
end
def clear_broadcast_messages
@messages_sent = {}
@messages_failed = {}
end
# Whenever a requirement fails, this is stored here.
def messages_failed
@messages_failed ||= {}
end
# This is called before_save
def set_broadcast_flags
@broadcasted = false
unless @skip_broadcasts
self.just_created = self.new_record?
self.prior_version = generate_prior_version
end
def changed_state(new_state=nil, old_state=nil)
begin
if new_state and old_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state == old_state.to_s
elsif new_state
self.workflow_state.to_s == new_state.to_s and
self.prior_version.workflow_state != self.workflow_state
else
self.workflow_state != self.prior_version.workflow_state
end
rescue Exception => e
logger.warn "Could not check if a record changed state: #{e.inspect}"
false
end
end
def generate_prior_version
obj = self.class.new
self.attributes.each do |attr, value|
obj.__send__("#{attr}=", value) rescue nil
end
alias :changed_state_to :changed_state
end # InstanceMethods
end # Policy
end # Adheres
self.changes.each do |attr, values|
obj.__send__("#{attr}=", values[0]) rescue nil
end
obj.workflow_state = self.workflow_state_was if obj.respond_to?("workflow_state=") && self.respond_to?("workflow_state_was")
obj
end
# This is called after_save
def broadcast_notifications
return if @broadcasted
@broadcasted = true
raise ArgumentError, "Broadcast Policy block not supplied for #{self.class.to_s}" unless self.class.broadcast_policy_list
self.class.broadcast_policy_list.broadcast(self)
end
attr_accessor :skip_broadcasts
def save_without_broadcasting
@skip_broadcasts = true
self.save
@skip_broadcasts = false
end
def save_without_broadcasting!
@skip_broadcasts = true
self.save!
@skip_broadcasts = false
end
# The rest of the methods here should just be helper methods to make
# writing a condition that much easier.
def changed_in_state(state, opts={})
fields = opts[:fields] || []
fields = [fields] unless fields.is_a?(Array)
# Come back to this to debug some of the notifications
# if fields == [:due_at]
# require 'rubygems'
# require 'ruby-debug'
# debugger
# 1 + 1
# end
begin
fields.map {|field| self.prior_version.send(field) != self.send(field) }.include?(true) and
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a change was made: #{e.inspect}"
false
end
end
def changed_in_states(states, opts={})
!states.select{|s| changed_in_state(s, opts)}.empty?
end
def remained_in_state(state)
begin
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a record remained in the same state: #{e.inspect}"
false
end
end
def changed_state(new_state=nil, old_state=nil)
begin
if new_state and old_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state == old_state.to_s
elsif new_state
self.workflow_state.to_s == new_state.to_s and
self.prior_version.workflow_state != self.workflow_state
else
self.workflow_state != self.prior_version.workflow_state
end
rescue Exception => e
logger.warn "Could not check if a record changed state: #{e.inspect}"
false
end
end
alias :changed_state_to :changed_state
end # InstanceMethods
end # BroadcastPolicy
end # Instructure

View File

@ -1,234 +0,0 @@
#
# 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/>.
#
# Commenting out the parts I'm now avoiding.
module Instructure #:nodoc:
module Broadcast #:nodoc:
# This should work like this:
#
# class Account < ActiveRecord::Base
# has_a_broadcast_policy
#
# set_broadcast_policy do
# dispatch(:name)
# to { some_list }
# whenever { |obj| obj.something == condition }
# end
# end
#
# Some useful examples:
#
# set_broadcast_policy do
# dispatch :new_assignment
# to { self.students }
# whenever { |record| record.just_created? }
# end
#
# set_broadcast_policy do
# dispatch :assignment_change
# to { self.students }
# whenever { |record|
# record.prior_version != self.version and true
# # ... some field-wise comparison
# }
# end
#
# u = User.find(:first)
# a = Account.find(:first)
# a.check_policy(u)
module Policy
# class PolicyStorage
#
# attr_accessor :dispatch, :to, :whenever
#
# def initialize(dispatch)
# self.dispatch = dispatch
# end
#
# # This should be called for an instance. It can only be sent out if the
# # condition is met, if there is a notification that we can find, and if
# # there is someone to send this to. At this point, a Message record is
# # created, which will be delayed, consolidated, dispatched to the right
# # server, and then finally sent through that server.
#
# def broadcast(record)
# begin
# meets_condition = self.whenever.call(record)
# rescue
# return false
# end
# return false unless meets_condition
#
# notification = Notification.find_by_name(self.dispatch)
# return false unless notification
# # self.consolidated_notifications[notification_name.to_s.titleize] rescue nil
#
# begin
# to_list = self.to.call(record)
# rescue
# return false
# end
# return false unless to_list
#
# notification.create_message(record, to_list)
# end
#
# end
module ClassMethods #:nodoc:
def has_a_broadcast_policy
# extend Instructure::Broadcast::Policy::SingletonMethods
include Instructure::Broadcast::Policy::InstanceMethods
after_save :broadcast_notifications # Must be defined locally...
before_save :set_broadcast_flags
end
# Uses the 'context' relationship as the governing relationship.
# Canonically, this probably will look something like:
# course -> professor -> section -> department -> account.
# So, this method recurses the list, keeping the nearer values. So,
# account can setup a series of default notifications, but a professor
# can override these.
# Removing for now, until we memoize this
# def consolidated_notifications
# instance_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, self.id)
# class_notifications = Notification.find_all_by_context_type_and_context_id(self.class.to_s, nil)
# context_notifications = context.consolidated_notifications if
# defined?(context) and context.respond_to?(:consolidated_notifications)
# context_notifications ||= []
#
# cn = hashify(*instance_notifications)
# cn.reverse_merge!(*class_notifications)
# cn.reverse_merge!(*context_notifications)
# cn
# end
#
# def hashify(*list)
# list.inject({}) {|h, v| h[v.name] = v; h}
# end
# protected :hashify
end
# This is where the DSL is defined.
# module SingletonMethods
#
# attr_accessor :broadcast_policy_block
#
# # This stores the policy for broadcasting changes on a class. It works like a
# # macro. The policy block will be stored in @broadcast_policy_block. Then, an
# # instance will use that to instantiate a Policy object.
# def set_broadcast_policy(&block)
# self.broadcast_policy_block = block
# end
#
# end
module InstanceMethods
attr_accessor :just_created, :prior_version
# This is called before_save
def set_broadcast_flags
self.just_created = self.new_record?
self.prior_version = self.versions.current.model rescue nil
end
# This is called after_save
# def broadcast_notifications
# self.instance_eval &self.class.broadcast_policy_block
# self.broadcast_policy_list.each {|p| p.broadcast(self) }
# end
# def broadcast_policy_list
# @broadcast_policy_list ||= []
# end
# If this is nil, we don't worry about trying to implement anything.
# def dispatch(notification_name)
# self.broadcast_policy_list << PolicyStorage.new(notification_name.to_s.titleize)
# end
# def implementing_policy
# if not self.broadcast_policy_list.last
# # This really shouldn't happen, if a policy is setup right, but it
# # should be logged and silently fail.
# self.broadcast_policy_list << PolicyStorage.new("unknown")
# end
# self.broadcast_policy_list.last
# end
#
# def to(&block)
# self.implementing_policy.to = block
# end
#
# def whenever(&block)
# self.implementing_policy.whenever = block
# end
# The rest of the methods here should just be helper methods to make
# writing a condition that much easier.
def changed_in_state(state, opts={})
fields = opts[:fields] || []
fields = [fields] unless fields.is_a?(Array)
begin
fields.each {|field| self.prior_version.send(field) != self.send(field) }.compact == [true] and
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a change was made: #{e.inspect}"
false
end
end
def remained_in_state(state)
begin
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a record remained in the same state: #{e.inspect}"
false
end
end
def changed_state(new_state=nil, old_state=nil)
begin
if new_state and old_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state == old_state.to_s
elsif new_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state != self.workflow_state
else
self.workflow_state != self.prior_version.workflow_state
end
rescue Exception => e
logger.warn "Could not check if a record changed state: #{e.inspect}"
false
end
end
alias :changed_state_to :changed_state
end # InstanceMethods
end # Policy
end # Adheres
end # Instructure

View File

@ -1,53 +0,0 @@
#
# 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/>.
#
module Instructure #:nodoc:
module Broadcast #:nodoc:
module Policy #:nodoc:
class PolicyStorage
attr_accessor :dispatch, :to, :whenever
def initialize(dispatch)
self.dispatch = dispatch
end
# This should be called for an instance. It can only be sent out if the
# condition is met, if there is a notification that we can find, and if
# there is someone to send this to. At this point, a Message record is
# created, which will be delayed, consolidated, dispatched to the right
# server, and then finally sent through that server.
def dispatch(record)
meets_condition = self.whenever.call(record)
return false unless meets_condition
notification = Notification.find_by_name(self.dispatch)
return false unless notification
# self.consolidated_notifications[notification_name.to_s.titleize] rescue nil
to_list = self.to.call(record)
return false unless to_list
notification.create_message(record, to_list)
end
end
end
end
end

View File

@ -1,90 +0,0 @@
#
# 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/>.
#
# This is a much simpler approach for now.
module Instructure #:nodoc:
module Broadcast #:nodoc:
module Policy
module ClassMethods #:nodoc:
def has_a_broadcast_policy
include Instructure::Broadcast::Policy::InstanceMethods
after_save :broadcast_notifications # Must be defined locally...
before_save :set_broadcast_flags
end
end
module InstanceMethods
attr_accessor :just_created, :prior_version
# This is called before_save
def set_broadcast_flags
self.just_created = self.new_record?
self.prior_version = self.versions.current.model rescue nil
end
# The rest of the methods here should just be helper methods to make
# writing a condition that much easier.
def changed_in_state(state, opts={})
fields = opts[:fields] || []
fields = [fields] unless fields.is_a?(Array)
begin
fields.each {|field| self.prior_version.send(field) != self.send(field) }.compact == [true] and
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a change was made: #{e.inspect}"
false
end
end
def remained_in_state(state)
begin
self.workflow_state == state.to_s and
self.prior_version.workflow_state == state.to_s
rescue Exception => e
logger.warn "Could not check if a record remained in the same state: #{e.inspect}"
false
end
end
def changed_state(new_state=nil, old_state=nil)
begin
if new_state and old_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state == old_state.to_s
elsif new_state
self.workflow_state == new_state.to_s and
self.prior_version.workflow_state != self.workflow_state
else
self.workflow_state != self.prior_version.workflow_state
end
rescue Exception => e
logger.warn "Could not check if a record changed state: #{e.inspect}"
false
end
end
alias :changed_state_to :changed_state
end # InstanceMethods
end # Policy
end # Adheres
end # Instructure

View File

@ -20,9 +20,9 @@ require 'rubygems'
require 'spec'
require File.join(File.dirname(__FILE__), "/../lib/broadcast_policy")
include ::Instructure::Broadcast
include ::Instructure
describe Policy, "set_broadcast_policy" do
describe BroadcastPolicy, "set_broadcast_policy" do
before(:each) do
class AnotherModel
class << self
@ -33,7 +33,7 @@ describe Policy, "set_broadcast_policy" do
true
end
end
extend Policy::ClassMethods
extend BroadcastPolicy::ClassMethods
end
end