notify_user 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +12 -0
  4. data/app/assets/javascripts/notify_user/application.js +15 -0
  5. data/app/assets/stylesheets/notify_user/application.css +13 -0
  6. data/app/controllers/notify_user/notifications_controller.rb +33 -0
  7. data/app/helpers/notify_user/application_helper.rb +4 -0
  8. data/app/mailers/notify_user/notification_mailer.rb +28 -0
  9. data/app/models/notify_user/base_notification.rb +187 -0
  10. data/app/serializers/notify_user/notification_serializer.rb +15 -0
  11. data/app/views/layouts/notify_user/application.html.erb +14 -0
  12. data/app/views/notify_user/action_mailer/aggregate_notification.html.erb +5 -0
  13. data/app/views/notify_user/action_mailer/notification.html.erb +1 -0
  14. data/config/routes.rb +6 -0
  15. data/lib/generators/notify_user/install/USAGE +8 -0
  16. data/lib/generators/notify_user/install/install_generator.rb +28 -0
  17. data/lib/generators/notify_user/install/templates/initializer.rb +12 -0
  18. data/lib/generators/notify_user/install/templates/migration.rb +13 -0
  19. data/lib/generators/notify_user/notification/USAGE +8 -0
  20. data/lib/generators/notify_user/notification/notification_generator.rb +16 -0
  21. data/lib/generators/notify_user/notification/templates/email_layout_template.html.erb.erb +33 -0
  22. data/lib/generators/notify_user/notification/templates/email_template.html.erb.erb +1 -0
  23. data/lib/generators/notify_user/notification/templates/mobile_sdk_template.html.erb.erb +1 -0
  24. data/lib/generators/notify_user/notification/templates/notification.rb.erb +10 -0
  25. data/lib/notify_user/channels/action_mailer/action_mailer_channel.rb +24 -0
  26. data/lib/notify_user/engine.rb +11 -0
  27. data/lib/notify_user/version.rb +3 -0
  28. data/lib/notify_user.rb +26 -0
  29. data/spec/controllers/notify_user/notifications_controller_spec.rb +54 -0
  30. data/spec/dummy/rails-4.0.2/Gemfile +45 -0
  31. data/spec/dummy/rails-4.0.2/README.rdoc +28 -0
  32. data/spec/dummy/rails-4.0.2/Rakefile +6 -0
  33. data/spec/dummy/rails-4.0.2/app/assets/javascripts/application.js +16 -0
  34. data/spec/dummy/rails-4.0.2/app/assets/stylesheets/application.css +13 -0
  35. data/spec/dummy/rails-4.0.2/app/controllers/application_controller.rb +5 -0
  36. data/spec/dummy/rails-4.0.2/app/helpers/application_helper.rb +2 -0
  37. data/spec/dummy/rails-4.0.2/app/models/user.rb +2 -0
  38. data/spec/dummy/rails-4.0.2/app/notifications/new_post_notification.rb +10 -0
  39. data/spec/dummy/rails-4.0.2/app/views/layouts/application.html.erb +14 -0
  40. data/spec/dummy/rails-4.0.2/app/views/notify_user/layouts/action_mailer.html.erb +33 -0
  41. data/spec/dummy/rails-4.0.2/app/views/notify_user/new_post_notification/action_mailer/notification.html.erb +1 -0
  42. data/spec/dummy/rails-4.0.2/app/views/notify_user/new_post_notification/mobile_sdk/notification.html.erb +1 -0
  43. data/spec/dummy/rails-4.0.2/bin/bundle +3 -0
  44. data/spec/dummy/rails-4.0.2/bin/rails +4 -0
  45. data/spec/dummy/rails-4.0.2/bin/rake +4 -0
  46. data/spec/dummy/rails-4.0.2/config/application.rb +23 -0
  47. data/spec/dummy/rails-4.0.2/config/boot.rb +4 -0
  48. data/spec/dummy/rails-4.0.2/config/database.yml +25 -0
  49. data/spec/dummy/rails-4.0.2/config/environment.rb +5 -0
  50. data/spec/dummy/rails-4.0.2/config/environments/development.rb +29 -0
  51. data/spec/dummy/rails-4.0.2/config/environments/production.rb +80 -0
  52. data/spec/dummy/rails-4.0.2/config/environments/test.rb +36 -0
  53. data/spec/dummy/rails-4.0.2/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/dummy/rails-4.0.2/config/initializers/filter_parameter_logging.rb +4 -0
  55. data/spec/dummy/rails-4.0.2/config/initializers/inflections.rb +16 -0
  56. data/spec/dummy/rails-4.0.2/config/initializers/mime_types.rb +5 -0
  57. data/spec/dummy/rails-4.0.2/config/initializers/notify_user.rb +12 -0
  58. data/spec/dummy/rails-4.0.2/config/initializers/secret_token.rb +12 -0
  59. data/spec/dummy/rails-4.0.2/config/initializers/session_store.rb +3 -0
  60. data/spec/dummy/rails-4.0.2/config/initializers/wrap_parameters.rb +14 -0
  61. data/spec/dummy/rails-4.0.2/config/locales/en.yml +23 -0
  62. data/spec/dummy/rails-4.0.2/config/routes.rb +56 -0
  63. data/spec/dummy/rails-4.0.2/config.ru +4 -0
  64. data/spec/dummy/rails-4.0.2/db/migrate/20140105070446_create_users.rb +9 -0
  65. data/spec/dummy/rails-4.0.2/db/migrate/20140105070448_create_notify_user_notifications.rb +13 -0
  66. data/spec/dummy/rails-4.0.2/db/schema.rb +32 -0
  67. data/spec/dummy/rails-4.0.2/db/seeds.rb +7 -0
  68. data/spec/dummy/rails-4.0.2/db/test.sqlite3 +0 -0
  69. data/spec/dummy/rails-4.0.2/log/test.log +1701 -0
  70. data/spec/dummy/rails-4.0.2/public/404.html +58 -0
  71. data/spec/dummy/rails-4.0.2/public/422.html +58 -0
  72. data/spec/dummy/rails-4.0.2/public/500.html +57 -0
  73. data/spec/dummy/rails-4.0.2/public/favicon.ico +0 -0
  74. data/spec/dummy/rails-4.0.2/public/robots.txt +5 -0
  75. data/spec/dummy/rails-4.0.2/test/fixtures/users.yml +7 -0
  76. data/spec/dummy/rails-4.0.2/test/models/user_test.rb +7 -0
  77. data/spec/dummy/rails-4.0.2/test/test_helper.rb +15 -0
  78. data/spec/factories/notify_user_notifications.rb +10 -0
  79. data/spec/fixtures/notify_user/notification_mailer/notification_email +3 -0
  80. data/spec/mailers/notify_user/notification_mailer_spec.rb +31 -0
  81. data/spec/models/notify_user/notification_spec.rb +71 -0
  82. data/spec/serializers/notify_user/notification_serializer_spec.rb +10 -0
  83. data/spec/setup_spec.rb +17 -0
  84. data/spec/spec_helper.rb +46 -0
  85. data/spec/support/rails_template.rb +8 -0
  86. metadata +289 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = NotifyUser
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler"
2
+ require 'rake'
3
+ Bundler.setup
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ def cmd(command)
7
+ puts command
8
+ raise unless system command
9
+ end
10
+
11
+ # Import all our rake tasks
12
+ FileList['tasks/**/*.rake'].each { |task| import task }
@@ -0,0 +1,15 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require jquery
14
+ //= require jquery_ujs
15
+ //= require_tree .
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,33 @@
1
+ class NotifyUser::NotificationsController < ApplicationController
2
+
3
+ before_filter :authenticate!
4
+
5
+ def index
6
+ @notifications = NotifyUser::BaseNotification.for_target(@user)
7
+ .order("created_at DESC")
8
+ .limit(30)
9
+ .page(params[:page])
10
+
11
+ render json: @notifications
12
+ end
13
+
14
+ def mark_read
15
+ @notifications = NotifyUser::BaseNotification.for_target(@user).where('id IN (?)', params[:ids])
16
+ @notifications.update_all(state: :read)
17
+ render json: @notifications
18
+ end
19
+
20
+ protected
21
+
22
+ def default_serializer_options
23
+ {
24
+ each_serializer: NotifyUser::NotificationSerializer,
25
+ template_renderer: self
26
+ }
27
+ end
28
+
29
+ def authenticate!
30
+ method(NotifyUser.authentication_method).call
31
+ @user = method(NotifyUser.current_user_method).call
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module NotifyUser
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,28 @@
1
+ module NotifyUser
2
+ class NotificationMailer < ActionMailer::Base
3
+
4
+ layout "notify_user/layouts/action_mailer"
5
+
6
+ def notification_email(notification, options)
7
+ @notification = notification
8
+
9
+ mail to: notification.target.email,
10
+ subject: options[:subject],
11
+ template_name: "notification",
12
+ template_path: "notify_user/action_mailer",
13
+ from: NotifyUser.mailer_sender
14
+ end
15
+
16
+ def aggregate_notifications_email(notifications, options)
17
+ @notifications = notifications
18
+
19
+ mail to: @notifications.first.target.email,
20
+ template_name: "aggregate_notification",
21
+ template_path: ["notify_user/#{notifications.first.class.name.underscore}/action_mailer", "notify_user/action_mailer"],
22
+ subject: options[:aggregate][:subject],
23
+ from: NotifyUser.mailer_sender
24
+ end
25
+
26
+ protected
27
+ end
28
+ end
@@ -0,0 +1,187 @@
1
+ require 'state_machine'
2
+ require 'sidekiq'
3
+
4
+ module NotifyUser
5
+ class BaseNotification < ActiveRecord::Base
6
+
7
+ if ActiveRecord::VERSION::MAJOR < 4
8
+ attr_accessible :params, :target, :type, :state
9
+ end
10
+
11
+ # Override point in case of collisions, plus keeps the table name tidy.
12
+ self.table_name = "notify_user_notifications"
13
+
14
+ # Params for creating the notification message.
15
+ serialize :params, Hash
16
+
17
+ # The user to send the notification to
18
+ belongs_to :target, polymorphic: true
19
+
20
+ validates_presence_of :target_id, :target_type, :target, :type, :state
21
+
22
+ state_machine :state, initial: :pending do
23
+
24
+ # Created, not sent yet. Possibly waiting for aggregation.
25
+ state :pending do
26
+ end
27
+
28
+ # Email/SMS/APNS has been sent.
29
+ state :sent do
30
+ end
31
+
32
+ # The user has seen this notification.
33
+ state :read do
34
+ end
35
+
36
+ # Record that we have sent message(s) to the user about this notification.
37
+ event :mark_as_sent do
38
+ transition :pending => :sent
39
+ end
40
+
41
+ # Record that the user has seen this notification, usually on a page or in the app.
42
+ # A notification can go straight from pending to read if it's seen in a view before
43
+ # sent in an email.
44
+ event :mark_as_read do
45
+ transition [:pending, :sent] => :read
46
+ end
47
+ end
48
+
49
+ ## Public Interface
50
+
51
+ def to(user)
52
+ self.target = user
53
+ self
54
+ end
55
+
56
+ def with(*args)
57
+ self.params = args.reduce({}, :update)
58
+ self
59
+ end
60
+
61
+ def notify!
62
+ save!
63
+
64
+ # Bang version of 'notify' ignores aggregation
65
+ self.deliver!
66
+ end
67
+
68
+ # Send any Emails/SMS/APNS
69
+ def notify
70
+
71
+ save!
72
+
73
+ if self.class.aggregate_per
74
+
75
+ # Schedule to send later if there aren't already any scheduled.
76
+ # Otherwise ignore, as the already-scheduled aggregate job will pick this one up when it runs.
77
+ if not aggregation_pending?
78
+
79
+ # Send in X minutes, along with any others created in the intervening times.
80
+ self.class.delay_for(self.class.aggregate_per).notify_aggregated(self.id)
81
+ end
82
+ else
83
+ # No aggregation, send immediately.
84
+ self.deliver
85
+ end
86
+
87
+ end
88
+
89
+ ## Channels
90
+
91
+ mattr_accessor :channels
92
+ @@channels = {
93
+ action_mailer: {},
94
+ }
95
+
96
+ # Not sure about this. The JSON and web feeds don't fit into channels, because nothing is broadcast through
97
+ # them. Not sure if they really need another concept though, they could just be formats on the controller.
98
+ mattr_accessor :views
99
+ @@views = {
100
+ mobile_sdk: {
101
+ template_path: Proc.new {|n| "notify_user/#{n.class.name.underscore}/mobile_sdk/notification" }
102
+ }
103
+ }
104
+
105
+ # Configure a channel
106
+ def self.channel(name, options={})
107
+ channels[name] = options
108
+ end
109
+
110
+ ## Aggregation
111
+
112
+ mattr_accessor :aggregate_per
113
+ @@aggregate_per = 1.minute
114
+
115
+ ## Sending
116
+
117
+ def self.for_target(target)
118
+ where(target_id: target.id)
119
+ .where(target_type: target.class.name)
120
+ end
121
+
122
+ def self.pending_aggregation_with(notification)
123
+ where(type: notification.type)
124
+ .for_target(notification.target)
125
+ .where(state: :pending)
126
+ end
127
+
128
+ def aggregation_pending?
129
+ # A notification of the same type, that would have an aggregation job associated with it,
130
+ # already exists.
131
+ return (self.class.pending_aggregation_with(self).where('id != ?', id).count > 0)
132
+ end
133
+
134
+ def deliver
135
+ self.mark_as_sent
136
+ self.save
137
+
138
+ self.class.delay.deliver_channels(self.id)
139
+ end
140
+
141
+ def deliver!
142
+ self.mark_as_sent
143
+ self.save
144
+ self.class.deliver_channels(self.id)
145
+ end
146
+
147
+ # Deliver a single notification across each channel.
148
+ def self.deliver_channels(notification_id)
149
+ notification = self.where(id: notification_id).first
150
+ return unless notification
151
+
152
+ self.channels.each do |channel_name, options|
153
+ channel = (channel_name.to_s + "_channel").camelize.constantize
154
+ channel.deliver(notification, options)
155
+ end
156
+ end
157
+
158
+ # Deliver multiple notifications across each channel as an aggregate message.
159
+ def self.deliver_channels_aggregated(notifications)
160
+ self.channels.each do |channel_name, options|
161
+ channel = (channel_name.to_s + "_channel").camelize.constantize
162
+ channel.deliver_aggregated(notifications, options)
163
+ end
164
+ end
165
+
166
+ def self.notify_aggregated(notification_id)
167
+ notification = self.find(notification_id) # Raise an exception if not found.
168
+
169
+ # Find any pending notifications with the same type and target, which can all be sent in one message.
170
+ notifications = self.pending_aggregation_with(notification)
171
+
172
+ notifications.map(&:mark_as_sent)
173
+ notifications.map(&:save)
174
+
175
+ return if notifications.empty?
176
+
177
+ if notifications.length == 1
178
+ # Despite waiting for more to aggregate, we only got one in the end.
179
+ self.deliver_channels(notifications.first.id)
180
+ else
181
+ # We got several notifications while waiting, send them aggregated.
182
+ self.deliver_channels_aggregated(notifications)
183
+ end
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,15 @@
1
+ class NotifyUser::NotificationSerializer < ActiveModel::Serializer
2
+ root :notifications
3
+
4
+ attributes :id, :message, :read
5
+
6
+ def message
7
+ options[:template_renderer].render_to_string(:template => object.class.views[:mobile_sdk][:template_path].call(object),
8
+ :locals => {params: object.params},
9
+ :layout => false)
10
+ end
11
+
12
+ def read
13
+ object.read?
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>NotifyUser</title>
5
+ <%= stylesheet_link_tag "notify_user/application", :media => "all" %>
6
+ <%= javascript_include_tag "notify_user/application" %>
7
+ <%= csrf_meta_tags %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,5 @@
1
+ <% @notifications.each do |notification| %>
2
+ <div>
3
+ <%= render template: "notify_user/#{notification.class.name.underscore}/action_mailer/notification", locals: { notification: notification, params: notification.params } %>
4
+ </div>
5
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render template: "notify_user/#{@notification.class.name.underscore}/action_mailer/notification", locals: { notification: @notification, params: @notification.params } %>
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ namespace :notify_user do
3
+ resources :notifications, only: [:index]
4
+ put 'notifications/mark_read' => 'notifications#mark_read'
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Creates a migration to set up the notifications table.
3
+
4
+ Example:
5
+ rails generate notify_user:install
6
+
7
+ This will create:
8
+ db/migrate/create_notifications.rb
@@ -0,0 +1,28 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ class NotifyUser::InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def copy_migration
9
+ migration_template "migration.rb", "db/migrate/create_notify_user_notifications"
10
+ puts "Installation successful. You can now run:"
11
+ puts " rake db:migrate"
12
+ end
13
+
14
+ def copy_initializer
15
+ template "initializer.rb", "config/initializers/notify_user.rb"
16
+ end
17
+
18
+ # This is defined in ActiveRecord::Generators::Base, but that inherits from NamedBase, so it expects a name argument
19
+ # which we don't want here. So we redefine it here. Yuck.
20
+ def self.next_migration_number(dirname)
21
+ if ActiveRecord::Base.timestamped_migrations
22
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
23
+ else
24
+ "%.3d" % (current_migration_number(dirname) + 1)
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,12 @@
1
+ NotifyUser.setup do |config|
2
+
3
+ # Override the email address from which notifications appear to be sent.
4
+ config.mailer_sender = "please-change-me-at-config-initializers-notify-user@example.com"
5
+
6
+ # NotifyUser will call this within NotificationsController to ensure the user is authenticated.
7
+ config.authentication_method = :authenticate_user!
8
+
9
+ # NotifyUser will call this within NotificationsController to return the current logged in user.
10
+ config.current_user_method = :current_user
11
+
12
+ end
@@ -0,0 +1,13 @@
1
+ class CreateNotifyUserNotifications < ActiveRecord::Migration
2
+ def change
3
+ create_table :notify_user_notifications do |t|
4
+ t.string :type
5
+ t.integer :target_id
6
+ t.string :target_type
7
+ t.text :params
8
+ t.string :state
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+ end