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
@@ -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
|