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