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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in eventifier.gemspec
4
+ gemspec
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
@@ -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