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