eventifier 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -0
- data/Gemfile.lock +83 -0
- data/README.textile +43 -0
- data/Rakefile +13 -0
- data/eventifier.gemspec +29 -0
- data/lib/eventifier.rb +42 -0
- data/lib/eventifier/event.rb +35 -0
- data/lib/eventifier/event_helper.rb +40 -0
- data/lib/eventifier/event_observer.rb +35 -0
- data/lib/eventifier/event_tracking.rb +81 -0
- data/lib/eventifier/helper_methods.rb +34 -0
- data/lib/eventifier/matchers.rb +34 -0
- data/lib/eventifier/notification.rb +40 -0
- data/lib/eventifier/notification_helper.rb +51 -0
- data/lib/eventifier/notification_mailer.rb +20 -0
- data/lib/eventifier/version.rb +3 -0
- data/lib/generators/active_record/eventifier_generator.rb +11 -0
- data/lib/generators/eventifier/install/install_generator.rb +25 -0
- data/lib/generators/eventifier/install/templates/event_tracking.rb +12 -0
- data/lib/generators/eventifier/install/templates/events.en.yaml +13 -0
- data/lib/generators/eventifier/install/templates/migration.rb +30 -0
- data/spec/event_helper_spec.rb +57 -0
- data/spec/event_observer_spec.rb +25 -0
- data/spec/event_spec.rb +28 -0
- data/spec/eventifier_spec.rb +179 -0
- data/spec/helper_methods_spec.rb +36 -0
- data/spec/integration/eventifier_spec.rb +67 -0
- data/spec/notification_helper_spec.rb +58 -0
- data/spec/notification_mailer_spec.rb +12 -0
- data/spec/notification_spec.rb +83 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/action_mailer.rb +3 -0
- data/spec/support/active_record_support.rb +63 -0
- data/spec/support/blueprints.rb +24 -0
- metadata +170 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
eventifier (0.0.1)
|
5
|
+
actionmailer
|
6
|
+
actionmailer
|
7
|
+
activerecord
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
actionmailer (3.2.5)
|
13
|
+
actionpack (= 3.2.5)
|
14
|
+
mail (~> 2.4.4)
|
15
|
+
actionpack (3.2.5)
|
16
|
+
activemodel (= 3.2.5)
|
17
|
+
activesupport (= 3.2.5)
|
18
|
+
builder (~> 3.0.0)
|
19
|
+
erubis (~> 2.7.0)
|
20
|
+
journey (~> 1.0.1)
|
21
|
+
rack (~> 1.4.0)
|
22
|
+
rack-cache (~> 1.2)
|
23
|
+
rack-test (~> 0.6.1)
|
24
|
+
sprockets (~> 2.1.3)
|
25
|
+
activemodel (3.2.5)
|
26
|
+
activesupport (= 3.2.5)
|
27
|
+
builder (~> 3.0.0)
|
28
|
+
activerecord (3.2.5)
|
29
|
+
activemodel (= 3.2.5)
|
30
|
+
activesupport (= 3.2.5)
|
31
|
+
arel (~> 3.0.2)
|
32
|
+
tzinfo (~> 0.3.29)
|
33
|
+
activesupport (3.2.5)
|
34
|
+
i18n (~> 0.6)
|
35
|
+
multi_json (~> 1.0)
|
36
|
+
arel (3.0.2)
|
37
|
+
builder (3.0.0)
|
38
|
+
diff-lcs (1.1.3)
|
39
|
+
erubis (2.7.0)
|
40
|
+
hike (1.2.1)
|
41
|
+
i18n (0.6.0)
|
42
|
+
journey (1.0.3)
|
43
|
+
machinist (2.0)
|
44
|
+
mail (2.4.4)
|
45
|
+
i18n (>= 0.4.0)
|
46
|
+
mime-types (~> 1.16)
|
47
|
+
treetop (~> 1.4.8)
|
48
|
+
mime-types (1.18)
|
49
|
+
multi_json (1.3.6)
|
50
|
+
pg (0.13.2)
|
51
|
+
polyglot (0.3.3)
|
52
|
+
rack (1.4.1)
|
53
|
+
rack-cache (1.2)
|
54
|
+
rack (>= 0.4)
|
55
|
+
rack-test (0.6.1)
|
56
|
+
rack (>= 1.0)
|
57
|
+
rspec (2.10.0)
|
58
|
+
rspec-core (~> 2.10.0)
|
59
|
+
rspec-expectations (~> 2.10.0)
|
60
|
+
rspec-mocks (~> 2.10.0)
|
61
|
+
rspec-core (2.10.1)
|
62
|
+
rspec-expectations (2.10.0)
|
63
|
+
diff-lcs (~> 1.1.3)
|
64
|
+
rspec-mocks (2.10.1)
|
65
|
+
sprockets (2.1.3)
|
66
|
+
hike (~> 1.2)
|
67
|
+
rack (~> 1.0)
|
68
|
+
tilt (~> 1.1, != 1.3.0)
|
69
|
+
tilt (1.3.3)
|
70
|
+
treetop (1.4.10)
|
71
|
+
polyglot
|
72
|
+
polyglot (>= 0.3.1)
|
73
|
+
tzinfo (0.3.33)
|
74
|
+
|
75
|
+
PLATFORMS
|
76
|
+
ruby
|
77
|
+
|
78
|
+
DEPENDENCIES
|
79
|
+
activerecord
|
80
|
+
eventifier!
|
81
|
+
machinist
|
82
|
+
pg
|
83
|
+
rspec
|
data/README.textile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
h1. Eventifier
|
2
|
+
|
3
|
+
Event tracking and notifying for active record models
|
4
|
+
|
5
|
+
h2. Features
|
6
|
+
|
7
|
+
Tracks and logs events on active record objects
|
8
|
+
Send notifications of events
|
9
|
+
|
10
|
+
h2. Example
|
11
|
+
|
12
|
+
class EventTracking
|
13
|
+
include Eventifier::EventTracking
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
events_for Post do
|
17
|
+
track_on [:create, :update, :destroy], :attributes => { :except => %w(updated_at) }
|
18
|
+
notify :group => :members, :on => [:create, :update]
|
19
|
+
end
|
20
|
+
|
21
|
+
events_for Announcement do
|
22
|
+
track_on :create, :attributes => { :except => %w(updated_at) }
|
23
|
+
notify :group => :members, :on => :create
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
That's it!
|
29
|
+
|
30
|
+
h2. Requirements
|
31
|
+
|
32
|
+
* ActiveRecord
|
33
|
+
|
34
|
+
|
35
|
+
h2. Testing
|
36
|
+
|
37
|
+
Creating the database:
|
38
|
+
* createdb eventifier
|
39
|
+
|
40
|
+
Spec
|
41
|
+
``
|
42
|
+
rspec spec
|
43
|
+
``
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rdoc/task'
|
6
|
+
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
11
|
+
spec.rcov = true
|
12
|
+
spec.rcov_opts = ['--exclude', 'spec', '--exclude', '.rvm']
|
13
|
+
end
|
data/eventifier.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "eventifier/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "eventifier"
|
7
|
+
s.version = Eventifier::VERSION
|
8
|
+
s.authors = ["Nathan Sampimon"]
|
9
|
+
s.email = ["nathan@inspire9.com"]
|
10
|
+
s.homepage = "http://github.com/inspire9/eventifier"
|
11
|
+
s.summary = "Event tracking and notifying for active record models"
|
12
|
+
s.description = "Tracks and logs events and sends notifications of events on Active Record models."
|
13
|
+
|
14
|
+
s.rubyforge_project = "eventifier"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "activerecord"
|
22
|
+
s.add_runtime_dependency "actionmailer"
|
23
|
+
s.add_development_dependency "machinist"
|
24
|
+
s.add_development_dependency "pg"
|
25
|
+
s.add_development_dependency "rspec"
|
26
|
+
|
27
|
+
s.add_runtime_dependency "activerecord"
|
28
|
+
s.add_runtime_dependency "actionmailer"
|
29
|
+
end
|
data/lib/eventifier.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Step 1. Use rails hooks for create and update and destroy
|
2
|
+
# Step 2. Use modules to overwrite methods, and be rails independent
|
3
|
+
# Consider implementing with http://stackoverflow.com/questions/3689736/rails-3-alias-method-chain-still-used
|
4
|
+
# Consider implementing with http://www.ruby-doc.org/stdlib-1.9.3/libdoc/observer/rdoc/Observable.html
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
# init.rb
|
9
|
+
# require 'eventifer'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
# Todo
|
16
|
+
# - Notifications
|
17
|
+
|
18
|
+
# Ideas for implementation:
|
19
|
+
|
20
|
+
|
21
|
+
# class EventTracking
|
22
|
+
# include Eventable::EventTracking
|
23
|
+
#
|
24
|
+
# def initialize
|
25
|
+
# events_for Activity,
|
26
|
+
# :on => [:create, :update, :destroy],
|
27
|
+
# :attributes => { :except => %w(updated_at) }
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# end
|
31
|
+
|
32
|
+
require 'active_record'
|
33
|
+
require 'action_mailer'
|
34
|
+
|
35
|
+
require 'eventifier/event'
|
36
|
+
require 'eventifier/helper_methods'
|
37
|
+
require 'eventifier/notification'
|
38
|
+
require 'eventifier/notification_mailer'
|
39
|
+
require 'eventifier/notification_helper'
|
40
|
+
require 'eventifier/event_helper'
|
41
|
+
require 'eventifier/event_observer'
|
42
|
+
require 'eventifier/event_tracking'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Eventifier
|
2
|
+
class Event < ::ActiveRecord::Base
|
3
|
+
|
4
|
+
belongs_to :user
|
5
|
+
belongs_to :eventable, :polymorphic => true
|
6
|
+
has_many :notifications
|
7
|
+
|
8
|
+
serialize :change_data
|
9
|
+
|
10
|
+
validates :user, :presence => true
|
11
|
+
validates :eventable, :presence => true
|
12
|
+
validates :verb, :presence => true
|
13
|
+
|
14
|
+
def self.add_notification(*arg)
|
15
|
+
observer_instances.each { |observer| observer.add_notification(*arg) }
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def self.create_event(verb, object, options = {})
|
20
|
+
changed_data = object.changes.stringify_keys
|
21
|
+
changed_data = changed_data.reject { |attribute, value| options[:except].include?(attribute) } if options[:except]
|
22
|
+
changed_data = changed_data.select { |attribute, value| options[:only].include?(attribute) } if options[:only]
|
23
|
+
self.create(
|
24
|
+
:user => object.user,
|
25
|
+
:eventable => object,
|
26
|
+
:verb => verb,
|
27
|
+
:change_data => changed_data.symbolize_keys
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.find_all_by_eventable object
|
32
|
+
where :eventable_id => object.id, :eventable_type => object.class.name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Eventifier
|
2
|
+
module EventHelper
|
3
|
+
include Eventifier::HelperMethods
|
4
|
+
|
5
|
+
# A helper for outputting an event message.
|
6
|
+
#
|
7
|
+
# Uses I18n messages from config/locales/events.en.yml to generate these messages, or defaults to a standard.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# %ul#events
|
12
|
+
# - post.events.each do |event|
|
13
|
+
# %li= event_message event
|
14
|
+
|
15
|
+
def event_message event
|
16
|
+
if event.verb.to_sym == :update
|
17
|
+
if event.change_data.keys.count == 1
|
18
|
+
key = "events.#{event.eventable_type.downcase}.#{event.verb}.single"
|
19
|
+
else
|
20
|
+
key = "events.#{event.eventable_type.downcase}.#{event.verb}.multiple"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
key = "events.#{event.eventable_type.downcase}.#{event.verb}"
|
24
|
+
end
|
25
|
+
message = I18n.translate key, :default => :"events.default.#{event.verb}", "user.name" => event.user.name, :"event.type" => event.eventable_type
|
26
|
+
|
27
|
+
replace_vars(message, event).html_safe
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.included(base)
|
31
|
+
base.helper_method :event_message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if defined? ActionController::Base
|
37
|
+
ActionController::Base.class_eval do
|
38
|
+
include Eventifier::EventHelper
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Eventifier
|
2
|
+
class EventObserver < ActiveRecord::Observer
|
3
|
+
observe Eventifier::Event
|
4
|
+
|
5
|
+
def add_notification klass_name, relation, method
|
6
|
+
observed_classes.each do |observed_class|
|
7
|
+
notification_mappings[klass_name.name] ||= {}
|
8
|
+
notification_mappings[klass_name.name][method] = relation
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def after_create event
|
13
|
+
Rails.logger.info "Firing #{event.eventable_type}##{event.verb} - #{notification_mappings[event.eventable_type][event.verb]}" if notification_mappings.has_key?(event.eventable_type) and notification_mappings[event.eventable_type].has_key?(event.verb) and defined?(Rails)
|
14
|
+
method_from_relation(event.eventable, notification_mappings[event.eventable_type][event.verb]).each do |user|
|
15
|
+
next if user == event.user
|
16
|
+
Eventifier::Notification.create :event => event, :user => user
|
17
|
+
end if notification_mappings.has_key?(event.eventable_type) and notification_mappings[event.eventable_type].has_key?(event.verb)
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_from_relation object, relation
|
21
|
+
if relation.kind_of?(Hash)
|
22
|
+
method_from_relation(proc { |object, method| object.send(method) }.call(object, relation.keys.first), relation.values.first)
|
23
|
+
else
|
24
|
+
send_to = proc { |object, method| object.send(method) }.call(object, relation)
|
25
|
+
send_to = send_to.kind_of?(Array) ? send_to : [send_to]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def notification_mappings
|
32
|
+
@notification_mapppings ||= {}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Eventifier
|
2
|
+
module EventTracking
|
3
|
+
def events_for(klass, *args, &block)
|
4
|
+
@klasses = klass.kind_of?(Array) ? klass : [klass]
|
5
|
+
|
6
|
+
options = args[0] || {}
|
7
|
+
|
8
|
+
methods = options.delete(:track_on)
|
9
|
+
attributes = options.delete(:attributes)
|
10
|
+
|
11
|
+
create_observers
|
12
|
+
|
13
|
+
if block.nil?
|
14
|
+
track_on methods, :attributes => attributes
|
15
|
+
else
|
16
|
+
instance_eval(&block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_observers
|
21
|
+
# Create all the observer classes
|
22
|
+
@klasses.each do |target_klass|
|
23
|
+
|
24
|
+
# If the observer doesn't exist, create the class
|
25
|
+
unless self.class.const_defined?("#{target_klass}Observer")
|
26
|
+
constant_name = "#{target_klass}Observer"
|
27
|
+
klass = Class.new(ActiveRecord::Observer)
|
28
|
+
self.class.qualified_const_set(constant_name, klass)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def track_on methods, options = {}
|
34
|
+
methods = methods.kind_of?(Array) ? methods : [methods]
|
35
|
+
attributes = options.delete(:attributes)
|
36
|
+
raise 'No events defined to track' if methods.compact.empty?
|
37
|
+
User.class_eval { has_many :notifications, :class_name => Eventifier::Notification } unless User.respond_to?(:notifications)
|
38
|
+
Eventifier::EventObserver.instance
|
39
|
+
|
40
|
+
# set up each class with an observer and relationships
|
41
|
+
@klasses.each do |target_klass|
|
42
|
+
# Add relations to class
|
43
|
+
target_klass.class_eval { has_many :events, :as => :eventable, :class_name => Eventifier::Event, :dependent => :destroy }
|
44
|
+
target_klass.class_eval { has_many :notifications, :through => :events, :class_name => Eventifier::Notification, :dependent => :destroy }
|
45
|
+
|
46
|
+
# create an observer and have it observe the class
|
47
|
+
klass = self.class.const_get("#{target_klass}Observer")
|
48
|
+
klass.observe target_klass
|
49
|
+
|
50
|
+
# create a callback for the methods we want to track
|
51
|
+
klass.class_eval do
|
52
|
+
methods.each do |event|
|
53
|
+
define_method "after_#{event}" do |object|
|
54
|
+
# create an event when the callback is fired
|
55
|
+
Eventifier::Event.create_event(event.to_sym, object, attributes) if object.changed?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
# instantiate the observer
|
60
|
+
klass.instance
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def notify *args
|
65
|
+
# args will either be [:relation, {:on => :create}] or [{:relation => :second_relation, :on => :create}]
|
66
|
+
# if its the first one, relation is the first in the array, otherwise treat the whole thing like a hash
|
67
|
+
relation = args.delete_at(0) if args.length == 2
|
68
|
+
args = args.first
|
69
|
+
|
70
|
+
methods = args.delete(:on)
|
71
|
+
methods = methods.kind_of?(Array) ? methods : [methods]
|
72
|
+
relation ||= args
|
73
|
+
|
74
|
+
@klasses.each do |target_klass|
|
75
|
+
methods.each do |method|
|
76
|
+
Eventifier::Event.add_notification target_klass, relation, method
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Eventifier
|
2
|
+
module HelperMethods
|
3
|
+
# Used to replace {{variables}} in I18n messages
|
4
|
+
|
5
|
+
def replace_vars message, event
|
6
|
+
event = load_event_for_template event
|
7
|
+
message.scan(/{{[^}]*}}/) do |replaceable|
|
8
|
+
method = "event."+replaceable.to_s.gsub(/[{|}]/, '').to_s
|
9
|
+
replace_text = eval(method) rescue ""
|
10
|
+
|
11
|
+
case replaceable.to_s
|
12
|
+
when "{{object.name}}"
|
13
|
+
replace_text = "<strong class='target'>#{replace_text}</strong>"
|
14
|
+
when "{{object.title}}"
|
15
|
+
replace_text = "<strong class='target'>#{replace_text}</strong>"
|
16
|
+
else
|
17
|
+
replace_text = "<strong>#{replace_text}</strong>"
|
18
|
+
end
|
19
|
+
|
20
|
+
message = message.gsub(replaceable.to_s, replace_text.to_s.gsub("_", " "))
|
21
|
+
end
|
22
|
+
message
|
23
|
+
end
|
24
|
+
|
25
|
+
# Make is so you can include object in the I18n descriptions and it refers to the eventable object the event is referring to.
|
26
|
+
|
27
|
+
def load_event_for_template event
|
28
|
+
def event.object; eventable; end
|
29
|
+
def event.object_type; eventable_type; end
|
30
|
+
|
31
|
+
event
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|