correspondent 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of correspondent might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +252 -0
- data/Rakefile +49 -0
- data/app/controllers/correspondent/application_controller.rb +6 -0
- data/app/controllers/correspondent/notifications_controller.rb +61 -0
- data/app/jobs/correspondent/application_job.rb +6 -0
- data/app/models/correspondent/application_record.rb +7 -0
- data/app/models/correspondent/notification.rb +89 -0
- data/config/routes.rb +15 -0
- data/lib/correspondent/engine.rb +8 -0
- data/lib/correspondent/version.rb +5 -0
- data/lib/correspondent.rb +144 -0
- data/lib/generators/correspondent/install/install_generator.rb +38 -0
- data/lib/generators/correspondent/install/templates/create_correspondent_notifications.rb +19 -0
- data/lib/tasks/correspondent_tasks.rake +6 -0
- metadata +268 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 98f21b4a1f595bbd6c95b5c62eeb15bf1fb719a31c02c40edd4bfc629dc36bd7
|
4
|
+
data.tar.gz: ee6d238830d85fd14e3e5a890c2653318678b1e31064e58bdaa80a535bcd9293
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 333981e1f0659f0965a66f8e7527c7d03b1f40e16ce707e846ce84286858c21f6b906cdc5fbf1682015afb30924249f7f99622784d36baab0fd2cfdb347d7861
|
7
|
+
data.tar.gz: cf1c9571f5312cce1b9b70529ffa6c55aef7048299c11d8e4846dc24ee93e508847d7f88d05d603186383b5c3c3c846b1b298f5c12fbbbdba1cbf5ec95dfd35a
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Vinicius Stock
|
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.md
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
[![Build Status](https://travis-ci.com/vinistock/correspondent.svg?branch=master)](https://travis-ci.com/vinistock/correspondent) [![Maintainability](https://api.codeclimate.com/v1/badges/07592c6d6b946a7b71fc/maintainability)](https://codeclimate.com/github/vinistock/correspondent/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/07592c6d6b946a7b71fc/test_coverage)](https://codeclimate.com/github/vinistock/correspondent/test_coverage) [![Gem Version](https://badge.fury.io/rb/correspondent.svg)](https://badge.fury.io/rb/correspondent) ![](http://ruby-gem-downloads-badge.herokuapp.com/correspondent?color=brightgreen&type=total)
|
2
|
+
|
3
|
+
# Correspondent
|
4
|
+
|
5
|
+
Dead simple configurable user notifications using the Correspondent engine!
|
6
|
+
|
7
|
+
Configure subscribers and publishers and let Correspondent deal with all notification work with very little overhead.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'correspondent'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
```bash
|
19
|
+
$ bundle
|
20
|
+
```
|
21
|
+
|
22
|
+
Create the necessary migrations:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
$ rails g correspondent:install
|
26
|
+
```
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
### Model configuration
|
31
|
+
|
32
|
+
Notifications can easily be setup using Correspondent. The following example goes through the basic usage. There are only two steps for the basic configuration:
|
33
|
+
|
34
|
+
1. Invoke notifies and configure the subscriber (user in this case), the triggers (method purchase in this case) and desired options
|
35
|
+
2. Define the to_notification method to configure information that needs to be used to create notifications
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# Example model using Correspondent
|
39
|
+
# app/models/purchase.rb
|
40
|
+
class Purchase < ApplicationRecord
|
41
|
+
belongs_to :user
|
42
|
+
belongs_to :store
|
43
|
+
|
44
|
+
# Notifies configuration
|
45
|
+
# First argument is the subscriber (the one that receives a notification). Can be an NxN association as well (e.g.: users) which will create a notification for each associated record.
|
46
|
+
# Second argument are the triggers (the method inside that model that triggers notifications). Can be an array of symbols for multiple triggers for the same entity.
|
47
|
+
# Third argument are generic options as a hash
|
48
|
+
notifies :user, :purchase, avoid_duplicates: true
|
49
|
+
|
50
|
+
# Many notifies definitions can be used for different subscribers
|
51
|
+
# In the following case, every time purchase is invoked the following will happen:
|
52
|
+
# 1. A notification will be created for `user`
|
53
|
+
# 2. A notification will be created for `store`
|
54
|
+
# 3. An email will be triggered using the `StoreMailer` (invoking a method called purchase_email)
|
55
|
+
notifies :store, :purchase, avoid_duplicates: true, mailer: StoreMailer
|
56
|
+
|
57
|
+
# `notifies` will hook into the desired triggers.
|
58
|
+
# Every time this method is invoked by an instance of Purchase
|
59
|
+
# a notification will be created in the database using the
|
60
|
+
# `to_notification` method. The handling of notifications is
|
61
|
+
# done asynchronously to cause as little overhead as possible.
|
62
|
+
def purchase
|
63
|
+
# some business logic
|
64
|
+
end
|
65
|
+
|
66
|
+
# The to_notification method returns the information to be
|
67
|
+
# used for creating a notification. This will be invoked automatically
|
68
|
+
# by the gem when a trigger occurs.
|
69
|
+
# When calling this method, entity and trigger will be passed. Entity
|
70
|
+
# is the subscriber (in this example, `user`). Trigger is the method
|
71
|
+
# that triggered the notification. With this approach, the hash
|
72
|
+
# built to pass information can vary based on different triggers.
|
73
|
+
# If entity and trigger will not be used, this can simply be defined as
|
74
|
+
#
|
75
|
+
# def to_notification(*)
|
76
|
+
# # some hash
|
77
|
+
# end
|
78
|
+
def to_notification(entity:, trigger:)
|
79
|
+
{
|
80
|
+
title: "Purchase ##{id} for #{entity} #{send(entity).name}",
|
81
|
+
content: "Congratulations on your recent #{trigger} of #{name}",
|
82
|
+
image_url: "",
|
83
|
+
link_url: "/purchases/#{id}",
|
84
|
+
referrer_url: "/stores/#{store.id}"
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Correspondent can also trigger emails if desired. To trigger emails, the mailer class should be passed as an object and should implement a method follwing the naming convention.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# app/models/purchase.rb
|
94
|
+
|
95
|
+
class Purchase < ApplicationRecord
|
96
|
+
belongs_to :user
|
97
|
+
|
98
|
+
# Pass the desired mailer in the `mailer:` option
|
99
|
+
notifies :user, :purchase, mailer: ApplicationMailer
|
100
|
+
|
101
|
+
def purchase
|
102
|
+
# some business logic
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# app/mailers/application_mailer.rb
|
107
|
+
class ApplicationMailer < ActionMailer::Base
|
108
|
+
default from: 'from@example.com'
|
109
|
+
layout 'mailer'
|
110
|
+
|
111
|
+
# The mailer should implement methods following the naming convention of
|
112
|
+
# #{trigger}_email(triggering_instance)
|
113
|
+
#
|
114
|
+
# In this case, the `trigger` is the method purchase, so Correspondent will look for
|
115
|
+
# the purchase_email method. It will always pass the instance that triggered the email
|
116
|
+
# as an argument.
|
117
|
+
def purchase_email(purchase)
|
118
|
+
@purchase = purchase
|
119
|
+
mail(to: purchase.user.email, subject: "Congratulations on the purchase of #{purchase.name}")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
To reference the created notifications in the desired model, use the following association:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# app/models/purchase.rb
|
128
|
+
|
129
|
+
class User < ApplicationRecord
|
130
|
+
has_many :purchases
|
131
|
+
has_many :notifications, class_name: "Correspondent::Notification", as: :subscriber
|
132
|
+
end
|
133
|
+
|
134
|
+
class Purchase < ApplicationRecord
|
135
|
+
belongs_to :user
|
136
|
+
has_many :notifications, class_name: "Correspondent::Notification", as: :publisher
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
If a specific column is not needed for your project, remove them from the generated migrations and don't return the respective attribute inside the to_notification method.
|
141
|
+
|
142
|
+
### Options
|
143
|
+
|
144
|
+
The available options, their default values and their explanations are listed below.
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
# Avoid duplicates
|
148
|
+
# Prevents creating new notifications if a non dismissed notification for the same publisher and same subscriber already exists
|
149
|
+
notifies :some_resouce, :trigger, avoid_duplicates: false
|
150
|
+
|
151
|
+
# Mailer
|
152
|
+
# The Mailer class that implements the desired mailer triggers to send emails. Default is nil (doesn't send emails).
|
153
|
+
notifies :some_resouce, :trigger, mailer: nil
|
154
|
+
|
155
|
+
# Email only
|
156
|
+
# For preventing the creation of notifications and only trigger emails, add the email_only option
|
157
|
+
notifies :some_resouce, :trigger, email_only: false
|
158
|
+
|
159
|
+
# Conditionals
|
160
|
+
# If or unless options can be passed either as procs/lambdas or symbols representing the name of a method
|
161
|
+
# These will be evaluated in an instance context, every time trigger is invoked
|
162
|
+
notifies :some_resource, :trigger, if: :should_be_notified?
|
163
|
+
|
164
|
+
notifies :some_resource, :trigger, unless: -> { should_be_notified? && is_eligible? }
|
165
|
+
```
|
166
|
+
|
167
|
+
### JSON API
|
168
|
+
|
169
|
+
Correspondent exposes a few APIs to be used for handling notification logic in the application.
|
170
|
+
|
171
|
+
All APIs use the `stale?` check. So if passing the If-None-Match header, the API will support returning 304 (not modified) if the collection hasn't changed.
|
172
|
+
|
173
|
+
```json
|
174
|
+
Parameters
|
175
|
+
|
176
|
+
:subscriber_type -> The subscriber resource name - not in plural (e.g.: user)
|
177
|
+
:subscriber_id -> The id of the subscriber
|
178
|
+
|
179
|
+
Index
|
180
|
+
|
181
|
+
Retrieves all non dismissed notifications for a given subscriber.
|
182
|
+
|
183
|
+
Request
|
184
|
+
GET /correspondent/:subscriber_type/:subscriber_id/notifications
|
185
|
+
|
186
|
+
Response
|
187
|
+
[
|
188
|
+
{
|
189
|
+
"id":20,
|
190
|
+
"title":"Purchase #1 for user user",
|
191
|
+
"content":"Congratulations on your recent purchase of purchase",
|
192
|
+
"image_url":"",
|
193
|
+
"dismissed":false,
|
194
|
+
"publisher_type":"Purchase",
|
195
|
+
"publisher_id":1,
|
196
|
+
"created_at":"2019-03-01T14:19:31.273Z",
|
197
|
+
"link_url":"/purchases/1",
|
198
|
+
"referrer_url":"/stores/1"
|
199
|
+
}
|
200
|
+
]
|
201
|
+
|
202
|
+
Preview
|
203
|
+
|
204
|
+
Returns total number of non dismissed notifications and the newest notification.
|
205
|
+
|
206
|
+
Request
|
207
|
+
GET /correspondent/:subscriber_type/:subscriber_id/notifications/preview
|
208
|
+
|
209
|
+
Response
|
210
|
+
{
|
211
|
+
"count": 3,
|
212
|
+
"notification": {
|
213
|
+
"id":20,
|
214
|
+
"title":"Purchase #1 for user user",
|
215
|
+
"content":"Congratulations on your recent purchase of purchase",
|
216
|
+
"image_url":"",
|
217
|
+
"dismissed":false,
|
218
|
+
"publisher_type":"Purchase",
|
219
|
+
"publisher_id":1,
|
220
|
+
"created_at":"2019-03-01T14:22:31.649Z",
|
221
|
+
"link_url":"/purchases/1",
|
222
|
+
"referrer_url":"/stores/1"
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
|
227
|
+
Dismiss
|
228
|
+
|
229
|
+
Dismisses a given notification.
|
230
|
+
|
231
|
+
Resquest
|
232
|
+
PUT /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id/dismiss
|
233
|
+
|
234
|
+
Response
|
235
|
+
STATUS no_content (204)
|
236
|
+
|
237
|
+
Destroy
|
238
|
+
|
239
|
+
Destroys a given notification.
|
240
|
+
|
241
|
+
Resquest
|
242
|
+
DELETE /correspondent/:subscriber_type/:subscriber_id/notifications/:notification_id
|
243
|
+
|
244
|
+
Response
|
245
|
+
STATUS no_content (204)
|
246
|
+
```
|
247
|
+
|
248
|
+
## Contributing
|
249
|
+
|
250
|
+
Contributions are very welcome! Don't hesitate to ask if you wish to contribute, but don't yet know how. Please refer to this simple [guideline].
|
251
|
+
|
252
|
+
[guideline]: https://github.com/vinistock/correspondent/blob/master/CONTRIBUTING.md
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler/setup"
|
5
|
+
rescue LoadError
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
7
|
+
end
|
8
|
+
|
9
|
+
require "rdoc/task"
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = "rdoc"
|
13
|
+
rdoc.title = "Correspondent"
|
14
|
+
rdoc.options << "--line-numbers"
|
15
|
+
rdoc.rdoc_files.include("README.md")
|
16
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
17
|
+
end
|
18
|
+
|
19
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
20
|
+
load "rails/tasks/engine.rake"
|
21
|
+
load "rails/tasks/statistics.rake"
|
22
|
+
|
23
|
+
require "bundler/gem_tasks"
|
24
|
+
require "rake/testtask"
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << "test"
|
28
|
+
t.pattern = Dir["test/**/*_test.rb"].reject { |path| path.include?("benchmarks") }
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
33
|
+
|
34
|
+
namespace :test do
|
35
|
+
Rake::TestTask.new(:benchmark) do |t|
|
36
|
+
t.libs << "test"
|
37
|
+
t.pattern = "test/benchmarks/**/*_test.rb"
|
38
|
+
t.verbose = false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :all do
|
43
|
+
system(
|
44
|
+
"brakeman && "\
|
45
|
+
"rake && "\
|
46
|
+
"rubocop --parallel && "\
|
47
|
+
"rails_best_practices"
|
48
|
+
)
|
49
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_dependency "correspondent/application_controller"
|
4
|
+
|
5
|
+
module Correspondent
|
6
|
+
# NotificationsController
|
7
|
+
#
|
8
|
+
# API for all notifications related
|
9
|
+
# endpoints.
|
10
|
+
class NotificationsController < ApplicationController
|
11
|
+
before_action :find_notification, only: %i[dismiss destroy]
|
12
|
+
|
13
|
+
# index
|
14
|
+
#
|
15
|
+
# Returns all notifications for a given subscriber.
|
16
|
+
def index
|
17
|
+
notifications = Correspondent::Notification.for_subscriber(params[:subscriber_type], params[:subscriber_id])
|
18
|
+
render(json: notifications) if stale?(notifications)
|
19
|
+
end
|
20
|
+
|
21
|
+
# preview
|
22
|
+
#
|
23
|
+
# Returns the newest notification and the total
|
24
|
+
# number of notifications for the given subscriber.
|
25
|
+
def preview
|
26
|
+
notifications = Correspondent::Notification.for_subscriber(params[:subscriber_type], params[:subscriber_id])
|
27
|
+
|
28
|
+
if stale?(notifications)
|
29
|
+
render(
|
30
|
+
json: {
|
31
|
+
count: notifications.count,
|
32
|
+
notification: notifications.limit(1).first
|
33
|
+
}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# dismiss
|
39
|
+
#
|
40
|
+
# Dismisses a given notification.
|
41
|
+
def dismiss
|
42
|
+
@notification&.dismiss!
|
43
|
+
head(:no_content)
|
44
|
+
end
|
45
|
+
|
46
|
+
# destroy
|
47
|
+
#
|
48
|
+
# Destroys a given notification.
|
49
|
+
def destroy
|
50
|
+
@notification&.destroy
|
51
|
+
head(:no_content)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def find_notification
|
57
|
+
@notification = Correspondent::Notification.select(:id)
|
58
|
+
.find_by(id: params[:id])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Correspondent
|
4
|
+
# Notification
|
5
|
+
#
|
6
|
+
# Model to hold all notification logic.
|
7
|
+
class Notification < ApplicationRecord
|
8
|
+
belongs_to :subscriber, polymorphic: true
|
9
|
+
belongs_to :publisher, polymorphic: true
|
10
|
+
|
11
|
+
validates_presence_of :publisher, :subscriber
|
12
|
+
before_destroy :delete_cache_entry
|
13
|
+
|
14
|
+
scope :not_dismissed, -> { where(dismissed: false) }
|
15
|
+
scope :by_parents, lambda { |subscriber, publisher|
|
16
|
+
select(:id)
|
17
|
+
.where(subscriber: subscriber, publisher: publisher)
|
18
|
+
.not_dismissed
|
19
|
+
}
|
20
|
+
|
21
|
+
scope :for_subscriber, lambda { |type, id|
|
22
|
+
not_dismissed
|
23
|
+
.where(subscriber_type: type.capitalize, subscriber_id: id)
|
24
|
+
.order(id: :desc)
|
25
|
+
}
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# create_for!
|
29
|
+
#
|
30
|
+
# Creates notification(s) for the given
|
31
|
+
# +instance+ of the publisher and given
|
32
|
+
# +entity+ (subscriber).
|
33
|
+
def create_for!(attrs, options = {})
|
34
|
+
attributes = attrs[:instance].to_notification(entity: attrs[:entity], trigger: attrs[:trigger])
|
35
|
+
attributes[:publisher] = attrs[:instance]
|
36
|
+
|
37
|
+
relation = attrs[:instance].send(attrs[:entity])
|
38
|
+
|
39
|
+
if relation.respond_to?(:each)
|
40
|
+
create_many!(attributes, relation, options)
|
41
|
+
else
|
42
|
+
create_single!(attributes, relation, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# create_many!
|
47
|
+
#
|
48
|
+
# Creates a notification for each
|
49
|
+
# record of the +relation+ so that
|
50
|
+
# a many to many relationship can
|
51
|
+
# notify all associated objects.
|
52
|
+
def create_many!(attributes, relation, options)
|
53
|
+
relation.each do |record|
|
54
|
+
unless options[:avoid_duplicates] && by_parents(record, attributes[:publisher]).exists?
|
55
|
+
create!(attributes.merge(subscriber: record))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# create_single!
|
61
|
+
#
|
62
|
+
# Creates a single notification for the
|
63
|
+
# passed entity.
|
64
|
+
def create_single!(attributes, relation, options)
|
65
|
+
attributes[:subscriber] = relation
|
66
|
+
create!(attributes) unless options[:avoid_duplicates] && by_parents(relation, attributes[:publisher]).exists?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private_class_method :create_many!, :create_single!
|
71
|
+
|
72
|
+
def as_json(*)
|
73
|
+
Rails.cache.fetch("correspondent_notification_#{id}") do
|
74
|
+
attributes.except("updated_at", "subscriber_type", "subscriber_id")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def dismiss!
|
79
|
+
delete_cache_entry
|
80
|
+
update_attribute(:dismissed, true)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def delete_cache_entry
|
86
|
+
Rails.cache.delete("correspondent_notification_#{id}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Correspondent::Engine.routes.draw do
|
4
|
+
scope path: ":subscriber_type/:subscriber_id" do
|
5
|
+
resources :notifications, only: %i[index destroy] do
|
6
|
+
collection do
|
7
|
+
get :preview
|
8
|
+
end
|
9
|
+
|
10
|
+
member do
|
11
|
+
put :dismiss
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "correspondent/engine"
|
4
|
+
require "async"
|
5
|
+
|
6
|
+
module Correspondent # :nodoc:
|
7
|
+
class << self
|
8
|
+
attr_writer :patched_methods
|
9
|
+
|
10
|
+
# patched_methods
|
11
|
+
#
|
12
|
+
# Hash with information about methods
|
13
|
+
# that need to be patched.
|
14
|
+
def patched_methods
|
15
|
+
@patched_methods ||= {}.with_indifferent_access
|
16
|
+
end
|
17
|
+
|
18
|
+
# trigger_email
|
19
|
+
#
|
20
|
+
# Calls the method of a given mailer using the
|
21
|
+
# trigger. Triggering only happens if a mailer
|
22
|
+
# has been passed as an option.
|
23
|
+
#
|
24
|
+
# Will invoke methods in this manner:
|
25
|
+
#
|
26
|
+
# MyMailer.send("make_purchase_email", #<Purchase id: 1...>)
|
27
|
+
def trigger_email(data)
|
28
|
+
data.dig(:options, :mailer).send("#{data[:trigger]}_email", data[:instance]).deliver_now
|
29
|
+
end
|
30
|
+
|
31
|
+
# <<
|
32
|
+
#
|
33
|
+
# Adds the notification creation and email sending
|
34
|
+
# as asynchronous tasks.
|
35
|
+
def <<(data)
|
36
|
+
Async do
|
37
|
+
unless data.dig(:options, :email_only)
|
38
|
+
Correspondent::Notification.create_for!(data.except(:options), data[:options])
|
39
|
+
end
|
40
|
+
|
41
|
+
trigger_email(data) if data.dig(:options, :mailer)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# should_notify?
|
46
|
+
#
|
47
|
+
# Evaluates the if and unless options within
|
48
|
+
# the context of a model instance.
|
49
|
+
def should_notify?(context, opt)
|
50
|
+
if opt[:if].present?
|
51
|
+
evaluate_conditional(context, opt[:if])
|
52
|
+
elsif opt[:unless].present?
|
53
|
+
!evaluate_conditional(context, opt[:unless])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# evaluate_conditional
|
58
|
+
#
|
59
|
+
# Evaluates if or unless regardless of
|
60
|
+
# whether it is a proc or a symbol.
|
61
|
+
def evaluate_conditional(context, if_or_unless)
|
62
|
+
if if_or_unless.is_a?(Proc)
|
63
|
+
context.instance_exec(&if_or_unless)
|
64
|
+
else
|
65
|
+
context.method(if_or_unless).call
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# notifies
|
71
|
+
#
|
72
|
+
# Hook to patch the desired methods +triggers+
|
73
|
+
# to asynchronously create notifications / emails.
|
74
|
+
#
|
75
|
+
# This will patch the methods +triggers+ to publish
|
76
|
+
# notifications using the method_added callback.
|
77
|
+
# Upon each +triggers+ method definition, the callback
|
78
|
+
# runs and patches the original method.
|
79
|
+
# If already patched, doesn't do anything (to avoid infinite loops).
|
80
|
+
|
81
|
+
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
82
|
+
def notifies(entity, triggers, options = {})
|
83
|
+
save_trigger_info(entity, triggers, options)
|
84
|
+
return if methods(false).include?(:method_added)
|
85
|
+
|
86
|
+
class_eval do
|
87
|
+
# Method patching
|
88
|
+
#
|
89
|
+
# For each trigger method
|
90
|
+
# 1. Capture unbound instance method
|
91
|
+
# 2. Add it to patched methods to avoid trying to patch it again
|
92
|
+
# 3. Undefine it to avoid re-definition warnings
|
93
|
+
# 4. Define method again invoking original implementation and
|
94
|
+
# inserting a new task in Async
|
95
|
+
def self.method_added(name)
|
96
|
+
if Correspondent.patched_methods.key?(name)
|
97
|
+
original_method = instance_method(name)
|
98
|
+
undef_method(name)
|
99
|
+
patch_info = Correspondent.patched_methods.delete(name)
|
100
|
+
|
101
|
+
define_method(name) do |*args|
|
102
|
+
original_method.bind(self).call(*args)
|
103
|
+
|
104
|
+
patch_info.each do |info|
|
105
|
+
next unless Correspondent.should_notify?(self, info[:options])
|
106
|
+
|
107
|
+
Async do
|
108
|
+
Correspondent << {
|
109
|
+
instance: self,
|
110
|
+
entity: info[:entity],
|
111
|
+
trigger: name,
|
112
|
+
options: info[:options]
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize
|
122
|
+
|
123
|
+
# ActiveRecord on load hook
|
124
|
+
#
|
125
|
+
# Extend the module after load so that
|
126
|
+
# model class methods are available.
|
127
|
+
ActiveSupport.on_load(:active_record) do
|
128
|
+
extend Correspondent
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# save_trigger_info
|
134
|
+
#
|
135
|
+
# Saves trigger information in hash for future patching.
|
136
|
+
def save_trigger_info(entity, triggers, options)
|
137
|
+
triggers = [triggers] unless triggers.is_a?(Array)
|
138
|
+
|
139
|
+
triggers.each do |trigger|
|
140
|
+
Correspondent.patched_methods[trigger] ||= []
|
141
|
+
Correspondent.patched_methods[trigger] << { entity: entity, options: options }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/migration"
|
4
|
+
|
5
|
+
module Correspondent
|
6
|
+
module Generators
|
7
|
+
# InstallGenerator
|
8
|
+
#
|
9
|
+
# Creates the necessary migrations to be able to
|
10
|
+
# use the engine.
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
12
|
+
include Rails::Generators::Migration
|
13
|
+
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
15
|
+
desc "Create Correspondent migrations"
|
16
|
+
|
17
|
+
def self.next_migration_number(_path)
|
18
|
+
if @prev_migration_nr
|
19
|
+
@prev_migration_nr += 1
|
20
|
+
else
|
21
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
@prev_migration_nr.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def copy_migrations
|
28
|
+
migration_template "create_correspondent_notifications.rb",
|
29
|
+
"db/migrate/create_correspondent_notifications.rb",
|
30
|
+
migration_version: migration_version
|
31
|
+
end
|
32
|
+
|
33
|
+
def migration_version
|
34
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateCorrespondentNotifications < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :correspondent_notifications do |t|
|
4
|
+
t.string :title
|
5
|
+
t.string :content
|
6
|
+
t.string :image_url
|
7
|
+
t.string :link_url
|
8
|
+
t.string :referrer_url
|
9
|
+
t.boolean :dismissed, default: false
|
10
|
+
t.string :publisher_type, null: false
|
11
|
+
t.integer :publisher_id, null: false
|
12
|
+
t.string :subscriber_type, null: false
|
13
|
+
t.integer :subscriber_id, null: false
|
14
|
+
t.index [:publisher_type, :publisher_id], name: "index_correspondent_on_publisher"
|
15
|
+
t.index [:subscriber_type, :subscriber_id], name: "index_correspondent_on_subscriber"
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: correspondent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vinicius Stock
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-06-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: benchmark-ips
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: brakeman
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: codeclimate-test-reporter
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: purdytest
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rails_best_practices
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: rubocop-performance
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: ruby-prof
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: simplecov
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 0.16.1
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 0.16.1
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: sqlite3
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "<"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: 1.4.0
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "<"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: 1.4.0
|
223
|
+
description: Dead simple configurable user notifications with little overhead.
|
224
|
+
email:
|
225
|
+
- vinicius.stock@outlook.com
|
226
|
+
executables: []
|
227
|
+
extensions: []
|
228
|
+
extra_rdoc_files: []
|
229
|
+
files:
|
230
|
+
- MIT-LICENSE
|
231
|
+
- README.md
|
232
|
+
- Rakefile
|
233
|
+
- app/controllers/correspondent/application_controller.rb
|
234
|
+
- app/controllers/correspondent/notifications_controller.rb
|
235
|
+
- app/jobs/correspondent/application_job.rb
|
236
|
+
- app/models/correspondent/application_record.rb
|
237
|
+
- app/models/correspondent/notification.rb
|
238
|
+
- config/routes.rb
|
239
|
+
- lib/correspondent.rb
|
240
|
+
- lib/correspondent/engine.rb
|
241
|
+
- lib/correspondent/version.rb
|
242
|
+
- lib/generators/correspondent/install/install_generator.rb
|
243
|
+
- lib/generators/correspondent/install/templates/create_correspondent_notifications.rb
|
244
|
+
- lib/tasks/correspondent_tasks.rake
|
245
|
+
homepage: https://github.com/vinistock/correspondent
|
246
|
+
licenses:
|
247
|
+
- MIT
|
248
|
+
metadata: {}
|
249
|
+
post_install_message:
|
250
|
+
rdoc_options: []
|
251
|
+
require_paths:
|
252
|
+
- lib
|
253
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - ">="
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: '0'
|
258
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
259
|
+
requirements:
|
260
|
+
- - ">="
|
261
|
+
- !ruby/object:Gem::Version
|
262
|
+
version: '0'
|
263
|
+
requirements: []
|
264
|
+
rubygems_version: 3.0.3
|
265
|
+
signing_key:
|
266
|
+
specification_version: 4
|
267
|
+
summary: Dead simple configurable user notifications with little overhead.
|
268
|
+
test_files: []
|