eventifier 0.0.14 → 0.1.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/README.textile +32 -4
  4. data/app/assets/javascripts/eventifier/notifications.coffee +29 -5
  5. data/app/assets/javascripts/eventifier/templates/dropdown.hamlc +1 -1
  6. data/app/assets/stylesheets/eventifier/notifications.scss +1 -1
  7. data/app/controllers/eventifier/preferences_controller.rb +7 -9
  8. data/app/helpers/eventifier/notification_helper.rb +1 -1
  9. data/app/helpers/eventifier/path_helper.rb +2 -2
  10. data/app/models/eventifier/event.rb +1 -3
  11. data/app/models/eventifier/notification.rb +0 -2
  12. data/app/models/eventifier/notification_setting.rb +0 -2
  13. data/app/views/eventifier/notifications/index.jbuilder +2 -2
  14. data/db/migrate/5_system_events.rb +9 -0
  15. data/eventifier.gemspec +7 -5
  16. data/lib/eventifier.rb +2 -0
  17. data/lib/eventifier/event_builder.rb +4 -1
  18. data/lib/eventifier/notification_translator.rb +27 -5
  19. data/lib/eventifier/notifier/notification_subscriber.rb +6 -6
  20. data/lib/eventifier/notifier/notifier.rb +5 -5
  21. data/lib/eventifier/preferences.rb +14 -0
  22. data/lib/eventifier/tracker.rb +7 -0
  23. data/lib/generators/eventifier/install/install_generator.rb +1 -1
  24. data/lib/generators/eventifier/install/templates/{events.en.yaml → events.en.yml} +3 -0
  25. data/spec/controllers/eventifier/notifications_controller_spec.rb +3 -1
  26. data/spec/controllers/eventifier/preferences_controller_spec.rb +5 -23
  27. data/spec/event_tracking_spec.rb +2 -2
  28. data/spec/eventifier/notification_translator_spec.rb +118 -0
  29. data/spec/eventifier/preferences_spec.rb +47 -10
  30. data/spec/eventifier/relationship_spec.rb +8 -8
  31. data/spec/eventifier_spec.rb +14 -10
  32. data/spec/helpers/eventifier/notification_helper_spec.rb +13 -13
  33. data/spec/helpers/eventifier/path_helper_spec.rb +3 -3
  34. data/spec/integration/eventifier_spec.rb +11 -11
  35. data/spec/integration/internationalisation_spec.rb +4 -4
  36. data/spec/integration/system_events_spec.rb +40 -0
  37. data/spec/models/eventifier/event_spec.rb +1 -1
  38. data/spec/models/eventifier/ghost_spec.rb +3 -4
  39. data/spec/models/eventifier/notification_spec.rb +3 -3
  40. data/spec/notifier/notification_mapping_spec.rb +3 -3
  41. metadata +61 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ddc5253dc949cf041ed9ff68c7225b234750fd4
4
- data.tar.gz: 81a0e4e9121a62a2de35453c7a301450e28d1ce7
3
+ metadata.gz: 86d60265a4396c1ceb7dbf3a127244ccd8dd2544
4
+ data.tar.gz: 64b679101dc154bd27a9dae71e152c20ed469446
5
5
  SHA512:
6
- metadata.gz: 6bf60d92b16aed0434bcfec3b9ab981eeec35b1b1c8c9d16595025f44124f41dc7c32ff061ac454fe768761fa5dfe27c31a5547199d80edcd3c017760c0a10e9
7
- data.tar.gz: a025c89c3fb8f3f07a8d778de602d030bda0cb3d7f86f13261dbf06bc281418528bea49d4f6d7c4ecd1538bdbf6afd6e34c2275c9d9a395b3572dd216a78c5dd
6
+ metadata.gz: 8e90e8779758969401a869fdd588de7647c9be752534e3ffcbe028c7d36401f7db1816dd8742392440e6ad291192ca1260d16b5b2f10fafeb632c68270382eb4
7
+ data.tar.gz: b2c9601e599c49d40808265111d2226b13379f5ebd3d465f40aed21235d041614471cc049fb3563f176745c19c439624661a15b252dfa3c430021946f9fd3d38
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in eventifier.gemspec
4
- gemspec
4
+ gemspec
data/README.textile CHANGED
@@ -17,13 +17,14 @@ class EventTracking
17
17
 
18
18
  def initialize
19
19
  events_for Post do
20
- track_on [:create, :update, :destroy], :attributes => { :except => %w(updated_at) }
21
- notify :group => :members, :on => [:create, :update]
20
+ track_on [:create, :update, :destroy], attributes: { except: %w(updated_at) }
21
+ notify group: :members, on: :create
22
+ notify group: :commenters, on: :update, unless: ->post,author { post.age > 1.week }
22
23
  end
23
24
 
24
25
  events_for Announcement do
25
- track_on :create, :attributes => { :except => %w(updated_at) }
26
- notify :group => :members, :on => :create
26
+ track_on :create
27
+ notify group: :members, on: :create, if: ->announcement,admin { announcement.is_important? }
27
28
  end
28
29
  end
29
30
  end
@@ -90,6 +91,21 @@ h4. Customise views
90
91
  #{object.user.username} commented on your post
91
92
  </code></pre>
92
93
 
94
+ You can create a custom view for each context
95
+
96
+ Dropdown notification: app/views/eventifier/dropdown/_comment.haml
97
+ Email notification: app/views/eventifier/email/_comment.haml
98
+
99
+ Helpers made available to these views are:
100
+
101
+ *notification*: The notification object
102
+ Eventifier::Notification(event: event, user: user_being_notified, parent: parent_notification, sent: email_sent_flag)
103
+
104
+ *event*: The event object
105
+ Eventifier::Event(user: event_owner, eventable: object_for_event, verb: [:create, :update, :destroy], change_data: {"name" => ["Bill", "Bob"]}, groupable: grouped_object)
106
+
107
+ *object*: The object the event was created for
108
+
93
109
  h2. Sending of emails
94
110
 
95
111
  Firstly, you'll need to set the FROM address for the Eventifier mailer
@@ -106,6 +122,18 @@ You want to add a scheduled task to run the following task every x minutes
106
122
  rake eventifier:email:deliver
107
123
  </code></pre>
108
124
 
125
+ h4. Customise email settings descriptions
126
+
127
+ <pre><code>
128
+ # config/locales/events.en.yml
129
+ en:
130
+ events:
131
+ labels:
132
+ preferences:
133
+ default: "All notifications"
134
+ create_relationships_notify_followed: "When you get a new follower"
135
+ </code></pre>
136
+
109
137
  h2. Requirements
110
138
 
111
139
  * ActiveRecord
@@ -9,9 +9,10 @@ class window.NotificationDropdown
9
9
  settingsTemplate: JST['eventifier/templates/settings']
10
10
 
11
11
  constructor: (options) ->
12
- {@el, @limit, @pollTime} = options
12
+ {@el, @limit, @pollTime, @push} = options
13
13
  @limit = @limit || 5
14
14
  @pollTime = @pollTime || 15
15
+ @push = @push || false
15
16
 
16
17
 
17
18
  [@notifications, @renderedNotifications, @unreadCount, @lastReadAt] = [[], [], 0, new Date()]
@@ -23,6 +24,7 @@ class window.NotificationDropdown
23
24
  @el.html(@template(@)).attr('tabindex', 0)
24
25
 
25
26
  # @checkVisibility()
27
+
26
28
  @setEvents()
27
29
  @poll()
28
30
 
@@ -48,11 +50,22 @@ class window.NotificationDropdown
48
50
  @el.on 'addNotifications', @renderNotifications
49
51
  @el.on 'addNotifications', @setUnreadCount
50
52
  @el.on 'poll', @poll
51
- @el.on 'scroll', 'ol', @scrolling
53
+ @el.find('ol.notifications_dropdown_list').on 'scroll', @scrolling
52
54
  $(window).on 'click', @blurNotifications
55
+ if @push
56
+ @el.on 'click', '#notification_dropdown ol a', @pushUrl
53
57
 
54
58
  @
55
59
 
60
+ pushUrl: (e)=>
61
+ location = $(e.currentTarget).attr('href')
62
+ location = $('<a />').attr(href: location).get(0).pathname if location.match /^https?\:\/\//
63
+
64
+ Backbone?.history.navigate(location, true) || history.pushState({trigger: true}, '', location)
65
+ @hide()
66
+
67
+ false
68
+
56
69
  renderNotifications: =>
57
70
  @el.find(".none").remove() if @notifications.length > 0
58
71
  $.each @notifications, (index, notification)=>
@@ -150,6 +163,17 @@ class window.NotificationDropdown
150
163
  @el.addClass('alerting')
151
164
 
152
165
  @el.find(".notification_alert").html(displayCount)
166
+ $('title').html (index, old_html) ->
167
+ if old_html.match /^\(\d+\).*/
168
+ if displayCount > 0
169
+ old_html.replace(/^\(\d+\)/, "(#{displayCount})");
170
+ else
171
+ old_html.replace(/^(\(\d+\))\s/, "");
172
+ else
173
+ if displayCount > 0
174
+ "(#{displayCount}) #{old_html}"
175
+ else
176
+ old_html
153
177
 
154
178
  setUnreadCount: =>
155
179
  @unreadCount = $.grep(@notifications, (notification)=>
@@ -187,13 +211,13 @@ class window.NotificationDropdown
187
211
  0
188
212
 
189
213
  lastLookTime: =>
190
- Math.max(@lastReadAt.getTime()/1000, @newestNotificationTime())
214
+ Math.max(@lastReadAt.getTime()/1000, @newestNotificationTime()/1000)
191
215
 
192
216
  scrolling: =>
193
- scrollWindow = @$el.find('ol')
217
+ scrollWindow = @el.find('ol')
194
218
 
195
219
  if (scrollWindow.scrollTop() + scrollWindow.innerHeight() >= scrollWindow[0].scrollHeight - 50)
196
- @loadMore(after: @oldestNotificationTime())
220
+ @loadMore(after: @oldestNotificationTime()/1000)
197
221
 
198
222
  arrayFromObject: (collection)->
199
223
  serializedObject = {}
@@ -5,6 +5,6 @@
5
5
  %header
6
6
  %a.toggle_settings{ href: '#' } Settings
7
7
  %h4 Notifications
8
- %ol
8
+ %ol.notifications_dropdown_list
9
9
  %li.none You have no notifications
10
10
  #settings_pane
@@ -149,7 +149,7 @@
149
149
  &.unread { background-color: #d3d3d3; }
150
150
  a {
151
151
  display: block;
152
- overflow: auto;
152
+ overflow: visible;
153
153
  padding-left: 38px;
154
154
  }
155
155
  img.avatar {
@@ -1,19 +1,17 @@
1
1
  class Eventifier::PreferencesController < Eventifier::ApplicationController
2
2
  def show
3
- render :json => Eventifier::Preferences.new(current_user).to_hashes
3
+ render :json => preferences.to_hashes
4
4
  end
5
5
 
6
6
  def update
7
- settings = Eventifier::NotificationSetting.for_user current_user
8
- settings.preferences['email'] ||= {}
9
- params[:preferences] ||= {}
7
+ preferences.update params[:preferences] || {}
10
8
 
11
- Eventifier::Preferences.new(current_user).to_hashes.each do |hash|
12
- settings.preferences['email'][hash[:key]] = !params[:preferences][hash[:key]].nil?
13
- end
9
+ render :json => {'status' => 'OK'}
10
+ end
14
11
 
15
- settings.save
12
+ private
16
13
 
17
- render :json => {'status' => 'OK'}
14
+ def preferences
15
+ Eventifier::Preferences.new(current_user)
18
16
  end
19
17
  end
@@ -37,7 +37,7 @@ module Eventifier
37
37
  else
38
38
  key = "events.#{event.eventable_type.downcase}.#{event.verb}"
39
39
  end
40
- message = I18n.translate key, :default => :"events.default.#{event.verb}", "user.name" => event.user.name, :"event.type" => event.eventable_type
40
+ message = I18n.translate key, :default => :"events.default.#{event.verb}", "user.name" => event.user.try(:name), :"event.type" => event.eventable_type
41
41
 
42
42
  replace_vars(message, event).html_safe
43
43
  end
@@ -5,11 +5,11 @@ module Eventifier
5
5
  end
6
6
 
7
7
  def partial_path notification, context = nil
8
- if lookup_context.exists?(notification.event.eventable_type.underscore, [:eventifier, context].compact.join("/"), true)
8
+ if lookup_context.exists?(notification.event.eventable_type.underscore, [[:eventifier, context].compact.join("/")], true)
9
9
  [:eventifier, context, notification.event.eventable_type.underscore].compact.join("/")
10
10
  else
11
11
  [:eventifier, context, 'notification'].compact.join("/")
12
12
  end
13
13
  end
14
14
  end
15
- end
15
+ end
@@ -2,15 +2,13 @@ module Eventifier
2
2
  class Event < ActiveRecord::Base
3
3
  self.table_name = 'eventifier_events'
4
4
 
5
- attr_accessible :user, :eventable, :verb, :change_data, :groupable
6
-
7
5
  belongs_to :user, class_name: Eventifier.user_model_name
8
6
  belongs_to :eventable, polymorphic: true
9
7
  belongs_to :groupable, polymorphic: true
10
8
  has_many :notifications, class_name: 'Eventifier::Notification',
11
9
  dependent: :destroy
12
10
 
13
- validates :user, presence: true
11
+ validates :user, presence: true, unless: :system?
14
12
  validates :eventable, presence: true
15
13
  validates :verb, presence: true
16
14
  validates :groupable, presence: true
@@ -2,8 +2,6 @@ module Eventifier
2
2
  class Notification < ActiveRecord::Base
3
3
  self.table_name = 'eventifier_notifications'
4
4
 
5
- attr_accessible :event, :user, :relations, :event_id, :user_id
6
-
7
5
  belongs_to :event, :class_name => 'Eventifier::Event'
8
6
  belongs_to :user
9
7
 
@@ -5,8 +5,6 @@ class Eventifier::NotificationSetting < ActiveRecord::Base
5
5
 
6
6
  serialize :preferences, MultiJson
7
7
 
8
- attr_accessible :user
9
-
10
8
  validates :user, :presence => true
11
9
  validates :user_id, :uniqueness => true
12
10
 
@@ -1,6 +1,6 @@
1
- json.last_read_at current_user.notifications_last_read_at.to_i
1
+ json.last_read_at current_user.notifications_last_read_at.to_i*1000
2
2
  json.notifications @notifications do |notification|
3
3
  json.(notification, :id)
4
- json.created_at notification.created_at.to_i
4
+ json.created_at notification.created_at.to_i*1000
5
5
  json.html render_partial_view(notification, :dropdown)
6
6
  end
@@ -0,0 +1,9 @@
1
+ class SystemEvents < ActiveRecord::Migration
2
+ def up
3
+ add_column :eventifier_events, :system, :boolean
4
+ end
5
+
6
+ def down
7
+ remove_column :eventifier_events, :system
8
+ end
9
+ end
data/eventifier.gemspec CHANGED
@@ -1,12 +1,13 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |s|
3
3
  s.name = "eventifier"
4
- s.version = '0.0.14'
4
+ s.version = '0.1.0'
5
5
  s.authors = ["Nathan Sampimon", "Peter Murray", "Pat Allan"]
6
6
  s.email = ["nathan@inspire9.com"]
7
7
  s.homepage = "http://github.com/inspire9/eventifier"
8
8
  s.summary = "Event tracking and notifying for active record models"
9
9
  s.description = "Tracks and logs events and sends notifications of events on Active Record models."
10
+ s.license = 'MIT'
10
11
 
11
12
  s.rubyforge_project = "eventifier"
12
13
 
@@ -15,17 +16,18 @@ Gem::Specification.new do |s|
15
16
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
17
  s.require_paths = ["lib"]
17
18
 
18
- s.add_runtime_dependency 'rails', '~> 3.2.0'
19
+ s.add_runtime_dependency 'rails', '~> 4.0.3'
19
20
  s.add_runtime_dependency "bson_ext"
20
21
  s.add_runtime_dependency 'haml-rails', '~> 0.4'
21
22
  s.add_runtime_dependency 'haml_coffee_assets'
22
- s.add_runtime_dependency 'coffee-rails', '~> 3.2.1'
23
+ s.add_runtime_dependency 'coffee-rails', '~> 4.0.0'
23
24
  s.add_runtime_dependency 'compass-rails'
24
25
  s.add_runtime_dependency 'multi_json', '~> 1.7.4'
25
26
  s.add_runtime_dependency 'jbuilder', '~> 1.4.2'
27
+ s.add_runtime_dependency 'rails-observers', '~> 0.1.2'
26
28
 
27
29
  s.add_development_dependency 'combustion', '~> 0.5.0'
28
- s.add_development_dependency 'fabrication', '~> 2.7.1'
30
+ s.add_development_dependency 'fabrication', '~> 2.11.0'
29
31
  s.add_development_dependency "pg"
30
- s.add_development_dependency 'rspec-rails', '~> 2.13.2'
32
+ s.add_development_dependency 'rspec-rails', '~> 3.0.0.beta2'
31
33
  end
data/lib/eventifier.rb CHANGED
@@ -9,6 +9,8 @@
9
9
  #
10
10
  # end
11
11
 
12
+ require "rails/observers/activerecord/active_record"
13
+
12
14
  require 'multi_json'
13
15
  require 'action_mailer'
14
16
 
@@ -10,7 +10,8 @@ class Eventifier::EventBuilder
10
10
 
11
11
  def store
12
12
  Eventifier::Event.create user: user, eventable: object,
13
- groupable: groupable, verb: verb, change_data: change_data
13
+ groupable: groupable, verb: verb, change_data: change_data,
14
+ system: options[:system]
14
15
  end
15
16
 
16
17
  private
@@ -18,6 +19,8 @@ class Eventifier::EventBuilder
18
19
  attr_reader :object, :user, :verb, :groupable, :options
19
20
 
20
21
  def change_data
22
+ return options[:change_data] unless options[:change_data].nil?
23
+
21
24
  changes = object.changes.stringify_keys
22
25
 
23
26
  changes.reject! { |attribute, value|
@@ -1,23 +1,45 @@
1
1
  class Eventifier::NotificationTranslator
2
- def initialize(prefix, delivery, *args)
3
- @prefix, @delivery = prefix, delivery
2
+ def initialize(prefix, options, *args)
3
+ @prefix, @options = prefix, options
4
4
  @event = ActiveSupport::Notifications::Event.new(*args).payload[:event]
5
5
  end
6
6
 
7
7
  def translate
8
+ return if skip?
8
9
  users_and_relations do |user, relations|
9
- next if user == event.user
10
+ next if user == event.user && !options[:notify_self]
11
+ next if skip?(user)
10
12
 
11
13
  Eventifier::Notification.create event: event, user: user,
12
14
  relations: relations
13
15
 
14
- Eventifier::Delivery.deliver_for user if delivery == :immediate
16
+ Eventifier::Delivery.deliver_for user if options[:email] == :immediate
15
17
  end
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- attr_reader :event, :prefix, :delivery
22
+ attr_reader :event, :prefix, :options
23
+
24
+ def skip?(user = nil)
25
+ if conditional
26
+ !conditional_call *[event.eventable, user][0..conditional.arity-1]
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def conditional
33
+ options[:if] || options[:unless]
34
+ end
35
+
36
+ def conditional_call(*args)
37
+ if options[:if]
38
+ conditional.call(*args)
39
+ else
40
+ !conditional.call(*args)
41
+ end
42
+ end
21
43
 
22
44
  def users_and_relations(&block)
23
45
  Eventifier::NotificationMapping.users_and_relations event, prefix, &block
@@ -1,23 +1,23 @@
1
1
  class Eventifier::NotificationSubscriber
2
- def self.subscribe_to_method(klass, method_name, delivery)
3
- new(klass, method_name, delivery).subscribe_to_method
2
+ def self.subscribe_to_method(klass, method_name, options)
3
+ new(klass, method_name, options).subscribe_to_method
4
4
  end
5
5
 
6
- def initialize(klass, method_name, delivery)
7
- @klass, @method_name, @delivery = klass, method_name, delivery
6
+ def initialize(klass, method_name, options)
7
+ @klass, @method_name, @options = klass, method_name, options
8
8
  end
9
9
 
10
10
  def subscribe_to_method
11
11
  return if notifications.notifier.listening?(name)
12
12
 
13
13
  notifications.subscribe(name) do |*args|
14
- Eventifier::NotificationTranslator.new(prefix, delivery, *args).translate
14
+ Eventifier::NotificationTranslator.new(prefix, options, *args).translate
15
15
  end
16
16
  end
17
17
 
18
18
  private
19
19
 
20
- attr_reader :klass, :method_name, :delivery
20
+ attr_reader :klass, :method_name, :options
21
21
 
22
22
  def name
23
23
  "#{prefix}.notification.eventifier"
@@ -1,21 +1,21 @@
1
1
  class Eventifier::Notifier
2
+ OPTION_KEYS = [:email, :if, :unless, :notify_self]
2
3
  # arguments will either be [:relation, {:on => :create}] or
3
4
  # [{:relation => :second_relation, :on => :create}]
4
5
  # If it's the first one, relation is the first in the array, otherwise treat
5
6
  # the whole thing like a hash
6
7
  def initialize(klasses, *arguments)
7
- relation = arguments.shift if arguments.length == 2
8
+ relation = arguments.shift if arguments.length >= 2
8
9
  arguments = arguments.first || {}
9
10
 
10
11
  methods = Array arguments.delete(:on)
11
- delivery = arguments.delete(:email) || :delayed
12
-
13
- relation ||= arguments
12
+ options = arguments.slice *OPTION_KEYS
13
+ relation ||= arguments.except *OPTION_KEYS
14
14
 
15
15
  klasses.each do |target_klass|
16
16
  methods.each do |method_name|
17
17
  Eventifier::NotificationMapping.add "#{method_name}.#{target_klass.name.tableize}", relation
18
- Eventifier::NotificationSubscriber.subscribe_to_method target_klass, method_name, delivery
18
+ Eventifier::NotificationSubscriber.subscribe_to_method target_klass, method_name, options
19
19
  end
20
20
  end
21
21
  end