notify_user 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/notify_user/application.js +15 -0
- data/app/assets/stylesheets/notify_user/application.css +13 -0
- data/app/controllers/notify_user/notifications_controller.rb +33 -0
- data/app/helpers/notify_user/application_helper.rb +4 -0
- data/app/mailers/notify_user/notification_mailer.rb +28 -0
- data/app/models/notify_user/base_notification.rb +187 -0
- data/app/serializers/notify_user/notification_serializer.rb +15 -0
- data/app/views/layouts/notify_user/application.html.erb +14 -0
- data/app/views/notify_user/action_mailer/aggregate_notification.html.erb +5 -0
- data/app/views/notify_user/action_mailer/notification.html.erb +1 -0
- data/config/routes.rb +6 -0
- data/lib/generators/notify_user/install/USAGE +8 -0
- data/lib/generators/notify_user/install/install_generator.rb +28 -0
- data/lib/generators/notify_user/install/templates/initializer.rb +12 -0
- data/lib/generators/notify_user/install/templates/migration.rb +13 -0
- data/lib/generators/notify_user/notification/USAGE +8 -0
- data/lib/generators/notify_user/notification/notification_generator.rb +16 -0
- data/lib/generators/notify_user/notification/templates/email_layout_template.html.erb.erb +33 -0
- data/lib/generators/notify_user/notification/templates/email_template.html.erb.erb +1 -0
- data/lib/generators/notify_user/notification/templates/mobile_sdk_template.html.erb.erb +1 -0
- data/lib/generators/notify_user/notification/templates/notification.rb.erb +10 -0
- data/lib/notify_user/channels/action_mailer/action_mailer_channel.rb +24 -0
- data/lib/notify_user/engine.rb +11 -0
- data/lib/notify_user/version.rb +3 -0
- data/lib/notify_user.rb +26 -0
- data/spec/controllers/notify_user/notifications_controller_spec.rb +54 -0
- data/spec/dummy/rails-4.0.2/Gemfile +45 -0
- data/spec/dummy/rails-4.0.2/README.rdoc +28 -0
- data/spec/dummy/rails-4.0.2/Rakefile +6 -0
- data/spec/dummy/rails-4.0.2/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/rails-4.0.2/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/rails-4.0.2/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/rails-4.0.2/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/rails-4.0.2/app/models/user.rb +2 -0
- data/spec/dummy/rails-4.0.2/app/notifications/new_post_notification.rb +10 -0
- data/spec/dummy/rails-4.0.2/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/rails-4.0.2/app/views/notify_user/layouts/action_mailer.html.erb +33 -0
- data/spec/dummy/rails-4.0.2/app/views/notify_user/new_post_notification/action_mailer/notification.html.erb +1 -0
- data/spec/dummy/rails-4.0.2/app/views/notify_user/new_post_notification/mobile_sdk/notification.html.erb +1 -0
- data/spec/dummy/rails-4.0.2/bin/bundle +3 -0
- data/spec/dummy/rails-4.0.2/bin/rails +4 -0
- data/spec/dummy/rails-4.0.2/bin/rake +4 -0
- data/spec/dummy/rails-4.0.2/config/application.rb +23 -0
- data/spec/dummy/rails-4.0.2/config/boot.rb +4 -0
- data/spec/dummy/rails-4.0.2/config/database.yml +25 -0
- data/spec/dummy/rails-4.0.2/config/environment.rb +5 -0
- data/spec/dummy/rails-4.0.2/config/environments/development.rb +29 -0
- data/spec/dummy/rails-4.0.2/config/environments/production.rb +80 -0
- data/spec/dummy/rails-4.0.2/config/environments/test.rb +36 -0
- data/spec/dummy/rails-4.0.2/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/rails-4.0.2/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/rails-4.0.2/config/initializers/inflections.rb +16 -0
- data/spec/dummy/rails-4.0.2/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/rails-4.0.2/config/initializers/notify_user.rb +12 -0
- data/spec/dummy/rails-4.0.2/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/rails-4.0.2/config/initializers/session_store.rb +3 -0
- data/spec/dummy/rails-4.0.2/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/rails-4.0.2/config/locales/en.yml +23 -0
- data/spec/dummy/rails-4.0.2/config/routes.rb +56 -0
- data/spec/dummy/rails-4.0.2/config.ru +4 -0
- data/spec/dummy/rails-4.0.2/db/migrate/20140105070446_create_users.rb +9 -0
- data/spec/dummy/rails-4.0.2/db/migrate/20140105070448_create_notify_user_notifications.rb +13 -0
- data/spec/dummy/rails-4.0.2/db/schema.rb +32 -0
- data/spec/dummy/rails-4.0.2/db/seeds.rb +7 -0
- data/spec/dummy/rails-4.0.2/db/test.sqlite3 +0 -0
- data/spec/dummy/rails-4.0.2/log/test.log +1701 -0
- data/spec/dummy/rails-4.0.2/public/404.html +58 -0
- data/spec/dummy/rails-4.0.2/public/422.html +58 -0
- data/spec/dummy/rails-4.0.2/public/500.html +57 -0
- data/spec/dummy/rails-4.0.2/public/favicon.ico +0 -0
- data/spec/dummy/rails-4.0.2/public/robots.txt +5 -0
- data/spec/dummy/rails-4.0.2/test/fixtures/users.yml +7 -0
- data/spec/dummy/rails-4.0.2/test/models/user_test.rb +7 -0
- data/spec/dummy/rails-4.0.2/test/test_helper.rb +15 -0
- data/spec/factories/notify_user_notifications.rb +10 -0
- data/spec/fixtures/notify_user/notification_mailer/notification_email +3 -0
- data/spec/mailers/notify_user/notification_mailer_spec.rb +31 -0
- data/spec/models/notify_user/notification_spec.rb +71 -0
- data/spec/serializers/notify_user/notification_serializer_spec.rb +10 -0
- data/spec/setup_spec.rb +17 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/rails_template.rb +8 -0
- 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
data/Rakefile
ADDED
@@ -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,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 @@
|
|
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,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
|