eventifier 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,34 @@
|
|
1
|
+
module Eventifier
|
2
|
+
module Matchers
|
3
|
+
class Notifications
|
4
|
+
def initialize(target)
|
5
|
+
@target = target
|
6
|
+
end
|
7
|
+
|
8
|
+
def matches?(block_to_test)
|
9
|
+
#before = incidents_count
|
10
|
+
existing_notifications = @target.inject({}) do |memo, record|
|
11
|
+
memo[record] = record.notifications
|
12
|
+
end
|
13
|
+
block_to_test.call
|
14
|
+
@target.none? do |record|
|
15
|
+
(existing_notifications[record] - record.notifications).empty?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def failure_message_for_should
|
20
|
+
'should have worked'
|
21
|
+
#"the block should have chronicled the '#{ @event_name }' incident for the #{ @target.class.name }##{ @target.object_id }, but it didn't"
|
22
|
+
end
|
23
|
+
|
24
|
+
def failure_message_for_should_not
|
25
|
+
'should not have worked'
|
26
|
+
#"the block should not have chronicled the '#{ @event_name }' incident for the #{ @target.class.name }##{ @target.object_id }, but it did"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_notifications_for(target)
|
31
|
+
Matchers::Notifications.new(target)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Eventifier
|
2
|
+
class Notification < ActiveRecord::Base
|
3
|
+
belongs_to :event
|
4
|
+
belongs_to :user
|
5
|
+
|
6
|
+
default_scope order("created_at DESC")
|
7
|
+
scope :for_events, lambda { |ids| where(:event_id => ids) }
|
8
|
+
scope :for_user, lambda { |id| where(:user_id => id) }
|
9
|
+
scope :latest, order('notifications.created_at DESC').limit(5)
|
10
|
+
|
11
|
+
validates :event, :presence => true
|
12
|
+
validates :user, :presence => true
|
13
|
+
validates :event_id, :uniqueness => { :scope => :user_id }
|
14
|
+
|
15
|
+
after_create :send_email
|
16
|
+
|
17
|
+
def self.expire_for_past_events!(time_limit = 1.day.ago)
|
18
|
+
self.for_events(Event.expired_ids(time_limit)).each &:expire!
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.unread_for(user)
|
22
|
+
if user.notifications_last_read_at
|
23
|
+
for_user(user).
|
24
|
+
where("notifications.created_at > ?", user.notifications_last_read_at)
|
25
|
+
else
|
26
|
+
for_user(user)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def unread_for?(user)
|
31
|
+
return true if user.notifications_last_read_at.nil?
|
32
|
+
created_at > user.notifications_last_read_at
|
33
|
+
end
|
34
|
+
|
35
|
+
def send_email
|
36
|
+
# TODO: Are we okay to have notifier sit on the gem? How is this going to be handled?
|
37
|
+
Eventifier::NotificationMailer.notification_email(self).deliver
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Eventifier
|
2
|
+
module NotificationHelper
|
3
|
+
include Eventifier::HelperMethods
|
4
|
+
|
5
|
+
# A helper for outputting a notification 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#recent_notifications
|
12
|
+
# - current_user.notifications.each do |notification|
|
13
|
+
# %li= notification_message notification.event
|
14
|
+
|
15
|
+
def notification_message event
|
16
|
+
if event.verb.to_sym == :update
|
17
|
+
if event.change_data.keys.count == 1
|
18
|
+
key = "notifications.#{event.eventable_type.downcase}.#{event.verb}.single"
|
19
|
+
else
|
20
|
+
key = "notifications.#{event.eventable_type.downcase}.#{event.verb}.multiple"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
key = "notifications.#{event.eventable_type.downcase}.#{event.verb}"
|
24
|
+
end
|
25
|
+
message = I18n.translate key, :default => :"notifications.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
|
+
# Generating where the notification should lead when clicked
|
31
|
+
|
32
|
+
def notification_url eventable
|
33
|
+
case eventable.class.name.downcase.to_sym
|
34
|
+
when :activity
|
35
|
+
url_for([eventable.group, eventable])
|
36
|
+
when :announcement
|
37
|
+
url_for([eventable.group, eventable.activity])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.included(base)
|
42
|
+
base.helper_method :notification_message, :notification_url
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if defined? ActionController::Base
|
48
|
+
ActionController::Base.class_eval do
|
49
|
+
include Eventifier::NotificationHelper
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'eventifier/notification_helper'
|
2
|
+
|
3
|
+
module Eventifier
|
4
|
+
class NotificationMailer < ActionMailer::Base
|
5
|
+
include Eventifier::NotificationHelper
|
6
|
+
layout 'email'
|
7
|
+
|
8
|
+
default :from => "\"Funways\" <noreply@funways.me>"
|
9
|
+
|
10
|
+
def notification_email(notification)
|
11
|
+
@notification = notification
|
12
|
+
@notification_url = notification_url(notification.event.eventable)
|
13
|
+
@notification_message = notification_message(notification.event).gsub("class='target'", "style='color: #ff0c50'").html_safe
|
14
|
+
|
15
|
+
mail :to => notification.user.email,
|
16
|
+
:subject => "You have new notifications"
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module Eventifier
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
extend ActiveRecord::Generators::Migration
|
8
|
+
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
10
|
+
|
11
|
+
def copy_event_tracking
|
12
|
+
copy_file "event_tracking.rb", "app/models/event_tracking.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def copy_language
|
16
|
+
copy_file "events.en.yaml", "config/locales/events.en.yaml"
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate_migration
|
20
|
+
migration_template "migration.rb", "db/migrate/eventifier_setup.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class EventTracking
|
2
|
+
include Eventifier::EventTracking
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
# events_for Group do
|
6
|
+
# track_on [:create, :update, :destroy], :attributes => { :except => %w(updated_at) }
|
7
|
+
# notify :group => :members, :on => [:create, :update]
|
8
|
+
# end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
EventTracking.new
|
@@ -0,0 +1,13 @@
|
|
1
|
+
en:
|
2
|
+
events:
|
3
|
+
default:
|
4
|
+
create: "{{user.name}} created a <strong>{{object_type}}</strong>"
|
5
|
+
update:
|
6
|
+
single: "{{user.name}} updated a {{object_type}}"
|
7
|
+
multiple: "{{user.name}} made some changes to a {{object_type}}"
|
8
|
+
notifications:
|
9
|
+
default:
|
10
|
+
create: "{{user.name}} created a <strong>{{object_type}}</strong>"
|
11
|
+
update:
|
12
|
+
single: "{{user.name}} updated a {{object_type}}"
|
13
|
+
multiple: "{{user.name}} made some changes to a {{object_type}}"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class EventifierSetup < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :events do |t|
|
4
|
+
t.integer :user_id
|
5
|
+
t.string :eventable_type
|
6
|
+
t.integer :eventable_id
|
7
|
+
t.string :verb
|
8
|
+
t.text :change_data
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :events, :user_id
|
14
|
+
add_index :events, [:eventable_id, :eventable_type]
|
15
|
+
|
16
|
+
create_table :notifications do |t|
|
17
|
+
t.integer :event_id
|
18
|
+
t.integer :user_id
|
19
|
+
t.integer :parent_id
|
20
|
+
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
add_index :notifications, :event_id
|
24
|
+
add_index :notifications, :user_id
|
25
|
+
add_index :notifications, :parent_id
|
26
|
+
|
27
|
+
add_column :users, :notifications_last_read_at, :datetime
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventifier::EventHelper do
|
4
|
+
|
5
|
+
class TestClass
|
6
|
+
def self.helper_method(*args); end
|
7
|
+
include Eventifier::EventHelper
|
8
|
+
end
|
9
|
+
|
10
|
+
let!(:helper) { TestClass.new }
|
11
|
+
|
12
|
+
before do
|
13
|
+
@event_strings = {
|
14
|
+
:post => {
|
15
|
+
:create => "{{user.name}} just created a new post - you should check it out",
|
16
|
+
:destroy => "{{user.name}} just deleted a post",
|
17
|
+
:update => {
|
18
|
+
:single => "{{user.name}} made a change to their post",
|
19
|
+
:multiple => "{{user.name}} made some changes to their post"
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
I18n.backend.store_translations :en, :events => @event_strings
|
24
|
+
end
|
25
|
+
|
26
|
+
describe ".event_message" do
|
27
|
+
it "should return the I18n message for that event" do
|
28
|
+
event = Eventifier::Event.make! :eventable => Post.make!, :verb => :create
|
29
|
+
helper.event_message(event).should == "<strong>#{event.user.name}</strong> just created a new post - you should check it out"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should return a message specific to a single change if only 1 change has been made" do
|
33
|
+
event = Eventifier::Event.make! :eventable => Post.make!, :verb => :update, :change_data => { :name => ["Fred", "Mike"] }
|
34
|
+
helper.event_message(event).should == "<strong>#{event.user.name}</strong> made a change to their post"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return a message specific to multiple changes if more than 1 change has been made" do
|
38
|
+
event = Eventifier::Event.make! :eventable => Post.make!, :verb => :update, :change_data => { :name => ["Fred", "Mike"], :age => [55, 65] }
|
39
|
+
helper.event_message(event).should == "<strong>#{event.user.name}</strong> made some changes to their post"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return the default I18n message if one doesn't exist" do
|
43
|
+
I18n.backend.reload!
|
44
|
+
@event_strings = {
|
45
|
+
:default => {
|
46
|
+
:create => "{{user.name}} created a {{eventable_type}}",
|
47
|
+
:update => "{{user.name}} updated a {{eventable_type}}"
|
48
|
+
}
|
49
|
+
}
|
50
|
+
I18n.backend.store_translations :test, :events => @event_strings
|
51
|
+
I18n.with_locale("test") do
|
52
|
+
event = Eventifier::Event.make! :eventable => Post.make!, :verb => :create
|
53
|
+
helper.event_message(event).should == "<strong>#{event.user.name}</strong> created a <strong>Post</strong>"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventifier::EventObserver do
|
4
|
+
|
5
|
+
describe "#add_notification" do
|
6
|
+
pending
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#method_from_relation" do
|
10
|
+
subject { Eventifier::EventObserver.instance }
|
11
|
+
|
12
|
+
it "should call the string as a method when passed a string" do
|
13
|
+
object = double('object', :mouse => 5)
|
14
|
+
|
15
|
+
subject.method_from_relation(object, :mouse).should == [5]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should string the methods when passed a hash" do
|
19
|
+
object = double('object', :cat => (cat = double('cat', :mouse => 5)))
|
20
|
+
|
21
|
+
subject.method_from_relation(object, :cat => :mouse).should == [5]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/spec/event_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventifier::Event do
|
4
|
+
let(:event) { Eventifier::Event.make! }
|
5
|
+
|
6
|
+
describe "#valid?" do
|
7
|
+
pending
|
8
|
+
#it_requires_a :user
|
9
|
+
#it_requires_an :eventable
|
10
|
+
#it_requires_a :verb
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".find_all_by_eventable" do
|
14
|
+
|
15
|
+
let!(:eventable) {Post.make!}
|
16
|
+
let(:event) {Eventifier::Event.make! :eventable => eventable}
|
17
|
+
|
18
|
+
it "should find the associated polymorphic eventable object" do
|
19
|
+
lambda do
|
20
|
+
Eventifier::Event.make! :eventable => Post.make!
|
21
|
+
event
|
22
|
+
end.should change(Eventifier::Event, :count).by(2)
|
23
|
+
|
24
|
+
Eventifier::Event.find_all_by_eventable(eventable).length.should == 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Eventifier::EventTracking do
|
4
|
+
|
5
|
+
let(:user) { User.make }
|
6
|
+
let(:test_class) { Post }
|
7
|
+
let(:object) { test_class.make }
|
8
|
+
let(:event_tracker) { Object.new.extend(Eventifier::EventTracking) }
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
Post.observers.disable :all
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ".initialize" do
|
15
|
+
|
16
|
+
it "should start" do
|
17
|
+
event_tracker
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ".events_for" do
|
23
|
+
|
24
|
+
it "should create the relation notifications for Users" do
|
25
|
+
event_tracker.events_for test_class,
|
26
|
+
:track_on => [:create, :update, :destroy],
|
27
|
+
:attributes => { :except => %w(updated_at) }
|
28
|
+
|
29
|
+
user.should respond_to(:notifications)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "hash syntax" do
|
35
|
+
|
36
|
+
describe "creating events" do
|
37
|
+
|
38
|
+
let(:subject) do
|
39
|
+
object.save
|
40
|
+
end
|
41
|
+
|
42
|
+
before do
|
43
|
+
object.stub(:user).and_return(user)
|
44
|
+
event_tracker.events_for test_class,
|
45
|
+
:track_on => [:create, :update, :destroy],
|
46
|
+
:attributes => { :except => %w(updated_at) }
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
it "should create an event with the relevant info" do
|
51
|
+
changes = {:foo => 'bar', :bar => 'baz'}
|
52
|
+
object.stub(:changes).and_return(changes)
|
53
|
+
|
54
|
+
Eventifier::Event.should_receive(:create).with(:user => user, :eventable => object, :verb => :create, :change_data => changes)
|
55
|
+
|
56
|
+
subject
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#create_event' do
|
63
|
+
let(:object) { double('object', :changes => changes, :user => double(User)) }
|
64
|
+
let(:changes) { {:foo => 'bar', :bar => 'baz', :bob => 'blah'} }
|
65
|
+
let(:options) { {} }
|
66
|
+
|
67
|
+
subject { Eventifier::Event.create_event(:create, object, options) }
|
68
|
+
|
69
|
+
describe "exclude" do
|
70
|
+
let(:options) { {:except => ['foo']} }
|
71
|
+
|
72
|
+
it 'should exclude the right attrs' do
|
73
|
+
Eventifier::Event.should_receive(:create) do |attrs|
|
74
|
+
attrs[:change_data].should == {:bar => 'baz', :bob => 'blah'}
|
75
|
+
end
|
76
|
+
subject
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
describe "only" do
|
81
|
+
let(:options) { {:only => ['foo']} }
|
82
|
+
|
83
|
+
it 'should exclude the right attrs' do
|
84
|
+
Eventifier::Event.should_receive(:create) do |attrs|
|
85
|
+
attrs[:change_data].should == {:foo => 'bar'}
|
86
|
+
end
|
87
|
+
subject
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
context "block syntax" do
|
96
|
+
|
97
|
+
context "tracking" do
|
98
|
+
let(:subject) do
|
99
|
+
object.save
|
100
|
+
end
|
101
|
+
|
102
|
+
before do
|
103
|
+
object.stub(:user).and_return(user)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should create an event with the relevant info" do
|
107
|
+
changes = {:foo => 'bar', :bar => 'baz'}
|
108
|
+
object.stub(:changes).and_return(changes)
|
109
|
+
event_tracker.events_for test_class do
|
110
|
+
track_on [:create, :destroy], :attributes => {:except => %w(updated_at)}
|
111
|
+
track_on :update, :attributes => {:except => %w(updated_at)}
|
112
|
+
end
|
113
|
+
|
114
|
+
Eventifier::Event.should_receive(:create).with(:user => user, :eventable => object, :verb => :create, :change_data => changes)
|
115
|
+
|
116
|
+
subject
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should hit the track event twice" do
|
120
|
+
event_tracker.should_receive(:track_on).exactly(2).times
|
121
|
+
|
122
|
+
event_tracker.events_for test_class do
|
123
|
+
track_on [:create, :destroy], :attributes => {:except => %w(updated_at)}
|
124
|
+
track_on :update, :attributes => {:except => %w(updated_at)}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "notifying" do
|
130
|
+
before do
|
131
|
+
@event_observer_instance = Eventifier::EventObserver.instance
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#add_notification" do
|
135
|
+
|
136
|
+
it "should add the notification to the notification hash" do
|
137
|
+
@event_observer_instance.should_receive(:add_notification).with(test_class, :user, :create)
|
138
|
+
|
139
|
+
event_tracker.events_for test_class do
|
140
|
+
track_on :create, :attributes => {:except => %w(updated_at)}
|
141
|
+
notify :user, :on => :create
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should notify users when passed a hash" do
|
147
|
+
subscriber = double('user')
|
148
|
+
|
149
|
+
object.stub :category => double('category', :subscribers => [subscriber])
|
150
|
+
|
151
|
+
Eventifier::Notification.should_receive(:create) do |args|
|
152
|
+
args[:user].should == subscriber
|
153
|
+
args[:event].eventable.should == object
|
154
|
+
end
|
155
|
+
|
156
|
+
event_tracker.events_for test_class do
|
157
|
+
track_on :create, :attributes => {:except => %w(updated_at)}
|
158
|
+
notify :category => :subscribers, :on => :create
|
159
|
+
end
|
160
|
+
|
161
|
+
object.save
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
it "should create a notification" do
|
166
|
+
Eventifier::Notification.should_receive(:create)
|
167
|
+
object.stub(:users => [User.make])
|
168
|
+
event_tracker.events_for test_class do
|
169
|
+
track_on :create, :attributes => {:except => %w(updated_at)}
|
170
|
+
notify :readers, :on => :create
|
171
|
+
end
|
172
|
+
|
173
|
+
object.save
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|