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