eventifier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +83 -0
  3. data/README.textile +43 -0
  4. data/Rakefile +13 -0
  5. data/eventifier.gemspec +29 -0
  6. data/lib/eventifier.rb +42 -0
  7. data/lib/eventifier/event.rb +35 -0
  8. data/lib/eventifier/event_helper.rb +40 -0
  9. data/lib/eventifier/event_observer.rb +35 -0
  10. data/lib/eventifier/event_tracking.rb +81 -0
  11. data/lib/eventifier/helper_methods.rb +34 -0
  12. data/lib/eventifier/matchers.rb +34 -0
  13. data/lib/eventifier/notification.rb +40 -0
  14. data/lib/eventifier/notification_helper.rb +51 -0
  15. data/lib/eventifier/notification_mailer.rb +20 -0
  16. data/lib/eventifier/version.rb +3 -0
  17. data/lib/generators/active_record/eventifier_generator.rb +11 -0
  18. data/lib/generators/eventifier/install/install_generator.rb +25 -0
  19. data/lib/generators/eventifier/install/templates/event_tracking.rb +12 -0
  20. data/lib/generators/eventifier/install/templates/events.en.yaml +13 -0
  21. data/lib/generators/eventifier/install/templates/migration.rb +30 -0
  22. data/spec/event_helper_spec.rb +57 -0
  23. data/spec/event_observer_spec.rb +25 -0
  24. data/spec/event_spec.rb +28 -0
  25. data/spec/eventifier_spec.rb +179 -0
  26. data/spec/helper_methods_spec.rb +36 -0
  27. data/spec/integration/eventifier_spec.rb +67 -0
  28. data/spec/notification_helper_spec.rb +58 -0
  29. data/spec/notification_mailer_spec.rb +12 -0
  30. data/spec/notification_spec.rb +83 -0
  31. data/spec/spec_helper.rb +8 -0
  32. data/spec/support/action_mailer.rb +3 -0
  33. data/spec/support/active_record_support.rb +63 -0
  34. data/spec/support/blueprints.rb +24 -0
  35. 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,3 @@
1
+ module Eventifier
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class EventifierGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path("../templates", __FILE__)
7
+
8
+
9
+ end
10
+ end
11
+ 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
@@ -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