noticed 1.2.20 → 1.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +61 -36
- data/lib/generators/noticed/model_generator.rb +11 -4
- data/lib/noticed.rb +1 -0
- data/lib/noticed/base.rb +7 -2
- data/lib/noticed/delivery_methods/email.rb +3 -5
- data/lib/noticed/engine.rb +9 -0
- data/lib/noticed/has_notifications.rb +49 -0
- data/lib/noticed/model.rb +4 -0
- data/lib/noticed/version.rb +1 -1
- data/lib/rails_6_polyfills/actioncable/test_adapter.rb +70 -0
- data/lib/rails_6_polyfills/actioncable/test_helper.rb +143 -0
- data/lib/rails_6_polyfills/activejob/serializers.rb +240 -0
- data/lib/rails_6_polyfills/base.rb +18 -0
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b24f360d71aca5620ed10866864570642c49d36d21e5068d3e92e02b56538a5
|
4
|
+
data.tar.gz: f22998f215bc9e75f14380bfabe5da7117476496395ecdb07da9f08952210bf6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1e02acbbcf4826441afa5ea04d30f949c528643e306fbc179320e76dfcf658c64b0db9c9f4ec8783aa624e49027ccc62108e5930fd003103378b4691215b87a
|
7
|
+
data.tar.gz: df2b7c0ddf9e13a6e059352a065ea8b3ea2e5c881499ae5227b32ebe5b645087829fd64e2c5cd48390c649075e1f3fe26eaa7e50d0ccea72dd18bc6a2dbeb3ae
|
data/README.md
CHANGED
@@ -265,7 +265,7 @@ Sends a Teams notification via webhook.
|
|
265
265
|
|
266
266
|
* `format: :format_for_teams` - *Optional*
|
267
267
|
|
268
|
-
Use a custom method to define the payload sent to
|
268
|
+
Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
|
269
269
|
Documentation for posting via Webhooks available at: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
|
270
270
|
|
271
271
|
```ruby
|
@@ -354,6 +354,33 @@ Sends an SMS notification via Vonage / Nexmo.
|
|
354
354
|
}
|
355
355
|
```
|
356
356
|
|
357
|
+
### Fallback Notifications
|
358
|
+
|
359
|
+
A common pattern is to deliver a notification via the database and then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the `delay` option, and the conditional `if` / `unless` option.
|
360
|
+
|
361
|
+
```ruby
|
362
|
+
class CommentNotification < Noticed::Base
|
363
|
+
deliver_by :database
|
364
|
+
deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
|
365
|
+
end
|
366
|
+
```
|
367
|
+
|
368
|
+
Here a notification will be created immediately in the database (for display directly in your app). If the notification has not been read after 15 minutes, the email notification will be sent. If the notification has already been read in the app, the email will be skipped.
|
369
|
+
|
370
|
+
You can also configure multiple fallback options:
|
371
|
+
|
372
|
+
```ruby
|
373
|
+
class CriticalSystemNotification < Noticed::Base
|
374
|
+
deliver_by :slack
|
375
|
+
deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, unless: :read?
|
376
|
+
deliver_by :twilio, delay: 20.minutes, unless: :read?
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
In this scenario, you can create an escalating notification that starts with a ping in Slack, then emails the team, and then finally sends an SMS to the on-call phone.
|
381
|
+
|
382
|
+
You can mix and match the options and delivery methods to suit your application specific needs.
|
383
|
+
|
357
384
|
### 🚚 Custom Delivery Methods
|
358
385
|
|
359
386
|
To generate a custom delivery method, simply run
|
@@ -444,13 +471,13 @@ Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The
|
|
444
471
|
deliver_by DeliveryMethods::Discord
|
445
472
|
```
|
446
473
|
|
447
|
-
For Rails 6.0, you must pass strings of the class names in the `deliver_by` options.
|
474
|
+
For Rails 5.2 and 6.0, you must pass strings of the class names in the `deliver_by` options.
|
448
475
|
|
449
476
|
```ruby
|
450
477
|
deliver_by :discord, class: "DeliveryMethods::Discord"
|
451
478
|
```
|
452
479
|
|
453
|
-
We recommend
|
480
|
+
We recommend using a string in order to prevent confusion.
|
454
481
|
|
455
482
|
### 📦 Database Model
|
456
483
|
|
@@ -507,57 +534,55 @@ Adding notification associations to your models makes querying and deleting noti
|
|
507
534
|
|
508
535
|
For example, in most cases, you'll want to delete notifications for records that are destroyed.
|
509
536
|
|
510
|
-
|
537
|
+
We'll need two associations for this:
|
511
538
|
|
512
|
-
|
539
|
+
1. Notifications where the record is the recipient
|
540
|
+
2. Notifications where the record is in the notification params
|
513
541
|
|
514
542
|
For example, we can query the notifications and delete them on destroy like so:
|
515
543
|
|
516
544
|
```ruby
|
517
545
|
class Post < ApplicationRecord
|
518
|
-
|
519
|
-
|
520
|
-
@notifications ||= Notification.where(params: { post: self })
|
521
|
-
|
522
|
-
# Or Postgres syntax to query the post key in the JSON column
|
523
|
-
# @notifications ||= Notification.where("params->'post' = ?", Noticed::Coder.dump(self).to_json)
|
524
|
-
end
|
546
|
+
# Standard association for deleting notifications when you're the recipient
|
547
|
+
has_many :notifications, as: :recipient, dependent: :destroy
|
525
548
|
|
526
|
-
|
549
|
+
# Helper for associating and destroying Notification records where(params: {post: self})
|
550
|
+
has_noticed_notifications
|
527
551
|
|
528
|
-
|
529
|
-
|
530
|
-
end
|
552
|
+
# You can override the param_name, the notification model name, or disable the before_destroy callback
|
553
|
+
has_noticed_notifications param_name: :parent, destroy: false, model: "Notification"
|
531
554
|
end
|
532
|
-
```
|
533
|
-
|
534
|
-
##### Polymorphic Association
|
535
555
|
|
536
|
-
|
556
|
+
# Create a CommentNotification with a post param
|
557
|
+
CommentNotification.with(post: @post).deliver(user)
|
558
|
+
# Lookup Notifications where params: {post: @post}
|
559
|
+
@post.notifications_as_post
|
537
560
|
|
538
|
-
|
539
|
-
|
540
|
-
|
561
|
+
CommentNotification.with(parent: @post).deliver(user)
|
562
|
+
@post.notifications_as_parent
|
563
|
+
```
|
541
564
|
|
542
|
-
|
565
|
+
#### Handling Deleted Records
|
543
566
|
|
544
|
-
|
545
|
-
class ExampleNotification < Noticed::Base
|
546
|
-
deliver_by :database, format: :format_for_database
|
567
|
+
If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discord the job on these errors by adding the following to `ApplicationJob`:
|
547
568
|
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
}
|
554
|
-
end
|
555
|
-
end
|
556
|
-
```
|
569
|
+
```ruby
|
570
|
+
class ApplicationJob < ActiveJob::Base
|
571
|
+
discard_on ActiveJob::DeserializationError
|
572
|
+
end
|
573
|
+
```
|
557
574
|
|
558
575
|
## 🙏 Contributing
|
559
576
|
|
560
577
|
This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
|
561
578
|
|
579
|
+
Running tests against multiple databases locally:
|
580
|
+
|
581
|
+
```
|
582
|
+
DATABASE_URL=sqlite3:noticed_test rails test
|
583
|
+
DATABASE_URL=mysql2://root:@127.0.0.1/noticed_test rails test
|
584
|
+
DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
|
585
|
+
```
|
586
|
+
|
562
587
|
## 📝 License
|
563
588
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -42,13 +42,20 @@ module Noticed
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def params_column
|
45
|
-
case
|
46
|
-
when "mysql2"
|
47
|
-
"params:json"
|
45
|
+
case current_adapter
|
48
46
|
when "postgresql"
|
49
47
|
"params:jsonb"
|
50
48
|
else
|
51
|
-
|
49
|
+
# MySQL and SQLite both support json
|
50
|
+
"params:json"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_adapter
|
55
|
+
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
56
|
+
ActiveRecord::Base.connection_db_config.adapter
|
57
|
+
else
|
58
|
+
ActiveRecord::Base.connection_config[:adapter]
|
52
59
|
end
|
53
60
|
end
|
54
61
|
end
|
data/lib/noticed.rb
CHANGED
@@ -5,6 +5,7 @@ require "noticed/engine"
|
|
5
5
|
module Noticed
|
6
6
|
autoload :Base, "noticed/base"
|
7
7
|
autoload :Coder, "noticed/coder"
|
8
|
+
autoload :HasNotifications, "noticed/has_notifications"
|
8
9
|
autoload :Model, "noticed/model"
|
9
10
|
autoload :TextCoder, "noticed/text_coder"
|
10
11
|
autoload :Translation, "noticed/translation"
|
data/lib/noticed/base.rb
CHANGED
@@ -12,6 +12,8 @@ module Noticed
|
|
12
12
|
# Gives notifications access to the record and recipient when formatting for delivery
|
13
13
|
attr_accessor :record, :recipient
|
14
14
|
|
15
|
+
delegate :read?, :unread?, to: :record
|
16
|
+
|
15
17
|
class << self
|
16
18
|
def deliver_by(name, options = {})
|
17
19
|
delivery_methods.push(name: name, options: options)
|
@@ -96,11 +98,14 @@ module Noticed
|
|
96
98
|
run_callbacks delivery_method[:name] do
|
97
99
|
method = delivery_method_for(delivery_method[:name], delivery_method[:options])
|
98
100
|
|
101
|
+
# If the queue is `nil`, ActiveJob will use a default queue name.
|
102
|
+
queue = delivery_method.dig(:options, :queue)
|
103
|
+
|
99
104
|
# Always perfrom later if a delay is present
|
100
105
|
if (delay = delivery_method.dig(:options, :delay))
|
101
|
-
method.set(wait: delay).perform_later(args)
|
106
|
+
method.set(wait: delay, queue: queue).perform_later(args)
|
102
107
|
elsif enqueue
|
103
|
-
method.perform_later(args)
|
108
|
+
method.set(queue: queue).perform_later(args)
|
104
109
|
else
|
105
110
|
method.perform_now(args)
|
106
111
|
end
|
@@ -18,14 +18,12 @@ module Noticed
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def format
|
21
|
-
if (method = options[:format])
|
21
|
+
params = if (method = options[:format])
|
22
22
|
notification.send(method)
|
23
23
|
else
|
24
|
-
notification.params
|
25
|
-
recipient: recipient,
|
26
|
-
record: record
|
27
|
-
)
|
24
|
+
notification.params
|
28
25
|
end
|
26
|
+
params.merge(recipient: recipient, record: record)
|
29
27
|
end
|
30
28
|
end
|
31
29
|
end
|
data/lib/noticed/engine.rb
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
module Noticed
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
+
initializer "noticed.has_notifications" do
|
4
|
+
ActiveSupport.on_load(:active_record) do
|
5
|
+
include Noticed::HasNotifications
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer "noticed.rails_5_2_support" do
|
10
|
+
require "rails_6_polyfills/base" if Rails::VERSION::MAJOR < 6
|
11
|
+
end
|
3
12
|
end
|
4
13
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Noticed
|
2
|
+
module HasNotifications
|
3
|
+
# Defines a method for the association and a before_destory callback to remove notifications
|
4
|
+
# where this record is a param
|
5
|
+
#
|
6
|
+
# class User < ApplicationRecord
|
7
|
+
# has_noticed_notifications
|
8
|
+
# has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# @user.notifications_as_user
|
12
|
+
# @user.notifications_as_owner
|
13
|
+
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
def has_noticed_notifications(param_name: model_name.singular, **options)
|
18
|
+
define_method "notifications_as_#{param_name}" do
|
19
|
+
model = options.fetch(:model_name, "Notification").constantize
|
20
|
+
case current_adapter
|
21
|
+
when "postgresql"
|
22
|
+
model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
|
23
|
+
when "mysql2"
|
24
|
+
model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
|
25
|
+
when "sqlite3"
|
26
|
+
model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
|
27
|
+
else
|
28
|
+
# This will perform an exact match which isn't ideal
|
29
|
+
model.where(params: {param_name.to_sym => self})
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if options.fetch(:destroy, true)
|
34
|
+
before_destroy do
|
35
|
+
send("notifications_as_#{param_name}").destroy_all
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def current_adapter
|
42
|
+
if ActiveRecord::Base.respond_to?(:connection_db_config)
|
43
|
+
ActiveRecord::Base.connection_db_config.adapter
|
44
|
+
else
|
45
|
+
ActiveRecord::Base.connection_config[:adapter]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/noticed/model.rb
CHANGED
@@ -24,12 +24,16 @@ module Noticed
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def noticed_coder
|
27
|
+
return Noticed::TextCoder unless table_exists?
|
28
|
+
|
27
29
|
case attribute_types["params"].type
|
28
30
|
when :json, :jsonb
|
29
31
|
Noticed::Coder
|
30
32
|
else
|
31
33
|
Noticed::TextCoder
|
32
34
|
end
|
35
|
+
rescue ActiveRecord::NoDatabaseError
|
36
|
+
Noticed::TextCoder
|
33
37
|
end
|
34
38
|
end
|
35
39
|
|
data/lib/noticed/version.rb
CHANGED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/subscription_adapter/base"
|
4
|
+
require "action_cable/subscription_adapter/subscriber_map"
|
5
|
+
require "action_cable/subscription_adapter/async"
|
6
|
+
|
7
|
+
module ActionCable
|
8
|
+
module SubscriptionAdapter
|
9
|
+
# == Test adapter for Action Cable
|
10
|
+
#
|
11
|
+
# The test adapter should be used only in testing. Along with
|
12
|
+
# <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
|
13
|
+
#
|
14
|
+
# To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
|
15
|
+
#
|
16
|
+
# NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
|
17
|
+
# so it could be used in system tests too.
|
18
|
+
class Test < Async
|
19
|
+
def broadcast(channel, payload)
|
20
|
+
broadcasts(channel) << payload
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def broadcasts(channel)
|
25
|
+
channels_data[channel] ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear_messages(channel)
|
29
|
+
channels_data[channel] = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@channels_data = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def channels_data
|
39
|
+
@channels_data ||= {}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update how broadcast_for determines the channel name so it's consistent with the Rails 6 way
|
45
|
+
module Channel
|
46
|
+
module Broadcasting
|
47
|
+
delegate :broadcast_to, to: :class
|
48
|
+
module ClassMethods
|
49
|
+
def broadcast_to(model, message)
|
50
|
+
ActionCable.server.broadcast(broadcasting_for(model), message)
|
51
|
+
end
|
52
|
+
|
53
|
+
def broadcasting_for(model)
|
54
|
+
serialize_broadcasting([channel_name, model])
|
55
|
+
end
|
56
|
+
|
57
|
+
def serialize_broadcasting(object) #:nodoc:
|
58
|
+
case # standard:disable Style/EmptyCaseCondition
|
59
|
+
when object.is_a?(Array)
|
60
|
+
object.map { |m| serialize_broadcasting(m) }.join(":")
|
61
|
+
when object.respond_to?(:to_gid_param)
|
62
|
+
object.to_gid_param
|
63
|
+
else
|
64
|
+
object.to_param
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
# Have ActionCable pick its Test SubscriptionAdapter when it's called for in cable.yml
|
5
|
+
module Server
|
6
|
+
class Configuration
|
7
|
+
def pubsub_adapter
|
8
|
+
cable["adapter"] == "test" ? ActionCable::SubscriptionAdapter::Test : super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Provides helper methods for testing Action Cable broadcasting
|
14
|
+
module TestHelper
|
15
|
+
def before_setup # :nodoc:
|
16
|
+
server = ActionCable.server
|
17
|
+
test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
|
18
|
+
|
19
|
+
@old_pubsub_adapter = server.pubsub
|
20
|
+
|
21
|
+
server.instance_variable_set(:@pubsub, test_adapter)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def after_teardown # :nodoc:
|
26
|
+
super
|
27
|
+
ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Asserts that the number of broadcasted messages to the stream matches the given number.
|
31
|
+
#
|
32
|
+
# def test_broadcasts
|
33
|
+
# assert_broadcasts 'messages', 0
|
34
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
35
|
+
# assert_broadcasts 'messages', 1
|
36
|
+
# ActionCable.server.broadcast 'messages', { text: 'world' }
|
37
|
+
# assert_broadcasts 'messages', 2
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# If a block is passed, that block should cause the specified number of
|
41
|
+
# messages to be broadcasted.
|
42
|
+
#
|
43
|
+
# def test_broadcasts_again
|
44
|
+
# assert_broadcasts('messages', 1) do
|
45
|
+
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# assert_broadcasts('messages', 2) do
|
49
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
50
|
+
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
def assert_broadcasts(stream, number)
|
55
|
+
if block_given?
|
56
|
+
original_count = broadcasts_size(stream)
|
57
|
+
yield
|
58
|
+
new_count = broadcasts_size(stream)
|
59
|
+
actual_count = new_count - original_count
|
60
|
+
else
|
61
|
+
actual_count = broadcasts_size(stream)
|
62
|
+
end
|
63
|
+
|
64
|
+
assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Asserts that no messages have been sent to the stream.
|
68
|
+
#
|
69
|
+
# def test_no_broadcasts
|
70
|
+
# assert_no_broadcasts 'messages'
|
71
|
+
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
72
|
+
# assert_broadcasts 'messages', 1
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# If a block is passed, that block should not cause any message to be sent.
|
76
|
+
#
|
77
|
+
# def test_broadcasts_again
|
78
|
+
# assert_no_broadcasts 'messages' do
|
79
|
+
# # No job messages should be sent from this block
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# Note: This assertion is simply a shortcut for:
|
84
|
+
#
|
85
|
+
# assert_broadcasts 'messages', 0, &block
|
86
|
+
#
|
87
|
+
def assert_no_broadcasts(stream, &block)
|
88
|
+
assert_broadcasts stream, 0, &block
|
89
|
+
end
|
90
|
+
|
91
|
+
# Asserts that the specified message has been sent to the stream.
|
92
|
+
#
|
93
|
+
# def test_assert_transmitted_message
|
94
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
95
|
+
# assert_broadcast_on('messages', text: 'hello')
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# If a block is passed, that block should cause a message with the specified data to be sent.
|
99
|
+
#
|
100
|
+
# def test_assert_broadcast_on_again
|
101
|
+
# assert_broadcast_on('messages', text: 'hello') do
|
102
|
+
# ActionCable.server.broadcast 'messages', text: 'hello'
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
def assert_broadcast_on(stream, data)
|
107
|
+
# Encode to JSON and back–we want to use this value to compare
|
108
|
+
# with decoded JSON.
|
109
|
+
# Comparing JSON strings doesn't work due to the order of the keys.
|
110
|
+
serialized_msg =
|
111
|
+
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
|
112
|
+
|
113
|
+
new_messages = broadcasts(stream)
|
114
|
+
if block_given?
|
115
|
+
old_messages = new_messages
|
116
|
+
clear_messages(stream)
|
117
|
+
|
118
|
+
yield
|
119
|
+
new_messages = broadcasts(stream)
|
120
|
+
clear_messages(stream)
|
121
|
+
|
122
|
+
# Restore all sent messages
|
123
|
+
(old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
|
124
|
+
end
|
125
|
+
|
126
|
+
message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
|
127
|
+
|
128
|
+
assert message, "No messages sent with #{data} to #{stream}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def pubsub_adapter # :nodoc:
|
132
|
+
ActionCable.server.pubsub
|
133
|
+
end
|
134
|
+
|
135
|
+
delegate :broadcasts, :clear_messages, to: :pubsub_adapter
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def broadcasts_size(channel)
|
140
|
+
broadcasts(channel).size
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# First add Rails 6.0 ActiveJob Serializers support, and then the
|
4
|
+
# DurationSerializer and SymbolSerializer.
|
5
|
+
module ActiveJob
|
6
|
+
module Arguments
|
7
|
+
# :nodoc:
|
8
|
+
OBJECT_SERIALIZER_KEY = "_aj_serialized"
|
9
|
+
|
10
|
+
def serialize_argument(argument)
|
11
|
+
case argument
|
12
|
+
when *TYPE_WHITELIST
|
13
|
+
argument
|
14
|
+
when GlobalID::Identification
|
15
|
+
convert_to_global_id_hash(argument)
|
16
|
+
when Array
|
17
|
+
argument.map { |arg| serialize_argument(arg) }
|
18
|
+
when ActiveSupport::HashWithIndifferentAccess
|
19
|
+
serialize_indifferent_hash(argument)
|
20
|
+
when Hash
|
21
|
+
symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
|
22
|
+
result = serialize_hash(argument)
|
23
|
+
result[SYMBOL_KEYS_KEY] = symbol_keys
|
24
|
+
result
|
25
|
+
when ->(arg) { arg.respond_to?(:permitted?) }
|
26
|
+
serialize_indifferent_hash(argument.to_h)
|
27
|
+
else # Add Rails 6 support for Serializers
|
28
|
+
Serializers.serialize(argument)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def deserialize_argument(argument)
|
33
|
+
case argument
|
34
|
+
when String
|
35
|
+
argument
|
36
|
+
when *TYPE_WHITELIST
|
37
|
+
argument
|
38
|
+
when Array
|
39
|
+
argument.map { |arg| deserialize_argument(arg) }
|
40
|
+
when Hash
|
41
|
+
if serialized_global_id?(argument)
|
42
|
+
deserialize_global_id argument
|
43
|
+
elsif custom_serialized?(argument)
|
44
|
+
Serializers.deserialize(argument)
|
45
|
+
else
|
46
|
+
deserialize_hash(argument)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def custom_serialized?(hash)
|
54
|
+
hash.key?(OBJECT_SERIALIZER_KEY)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
|
59
|
+
# and to add new ones. It also has helpers to serialize/deserialize objects.
|
60
|
+
module Serializers # :nodoc:
|
61
|
+
# Base class for serializing and deserializing custom objects.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
|
66
|
+
# def serialize(money)
|
67
|
+
# super("amount" => money.amount, "currency" => money.currency)
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# def deserialize(hash)
|
71
|
+
# Money.new(hash["amount"], hash["currency"])
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# private
|
75
|
+
#
|
76
|
+
# def klass
|
77
|
+
# Money
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
class ObjectSerializer
|
81
|
+
include Singleton
|
82
|
+
|
83
|
+
class << self
|
84
|
+
delegate :serialize?, :serialize, :deserialize, to: :instance
|
85
|
+
end
|
86
|
+
|
87
|
+
# Determines if an argument should be serialized by a serializer.
|
88
|
+
def serialize?(argument)
|
89
|
+
argument.is_a?(klass)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Serializes an argument to a JSON primitive type.
|
93
|
+
def serialize(hash)
|
94
|
+
{Arguments::OBJECT_SERIALIZER_KEY => self.class.name}.merge!(hash)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Deserializes an argument from a JSON primitive type.
|
98
|
+
def deserialize(_argument)
|
99
|
+
raise NotImplementedError
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
# The class of the object that will be serialized.
|
105
|
+
def klass
|
106
|
+
raise NotImplementedError
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class DurationSerializer < ObjectSerializer # :nodoc:
|
111
|
+
def serialize(duration)
|
112
|
+
super("value" => duration.value, "parts" => Arguments.serialize(duration.parts.each_with_object({}) { |v, s| s[v.first.to_s] = v.last }))
|
113
|
+
end
|
114
|
+
|
115
|
+
def deserialize(hash)
|
116
|
+
value = hash["value"]
|
117
|
+
parts = Arguments.deserialize(hash["parts"])
|
118
|
+
|
119
|
+
klass.new(value, parts)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def klass
|
125
|
+
ActiveSupport::Duration
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class SymbolSerializer < ObjectSerializer # :nodoc:
|
130
|
+
def serialize(argument)
|
131
|
+
super("value" => argument.to_s)
|
132
|
+
end
|
133
|
+
|
134
|
+
def deserialize(argument)
|
135
|
+
argument["value"].to_sym
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def klass
|
141
|
+
Symbol
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# -----------------------------
|
146
|
+
|
147
|
+
mattr_accessor :_additional_serializers
|
148
|
+
self._additional_serializers = Set.new
|
149
|
+
|
150
|
+
class << self
|
151
|
+
# Returns serialized representative of the passed object.
|
152
|
+
# Will look up through all known serializers.
|
153
|
+
# Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
|
154
|
+
def serialize(argument)
|
155
|
+
serializer = serializers.detect { |s| s.serialize?(argument) }
|
156
|
+
raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
|
157
|
+
serializer.serialize(argument)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns deserialized object.
|
161
|
+
# Will look up through all known serializers.
|
162
|
+
# If no serializer found will raise <tt>ArgumentError</tt>.
|
163
|
+
def deserialize(argument)
|
164
|
+
serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
|
165
|
+
raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
|
166
|
+
|
167
|
+
serializer = serializer_name.safe_constantize
|
168
|
+
raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
|
169
|
+
|
170
|
+
serializer.deserialize(argument)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns list of known serializers.
|
174
|
+
def serializers
|
175
|
+
self._additional_serializers # standard:disable Style/RedundantSelf
|
176
|
+
end
|
177
|
+
|
178
|
+
# Adds new serializers to a list of known serializers.
|
179
|
+
def add_serializers(*new_serializers)
|
180
|
+
self._additional_serializers += new_serializers.flatten
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
add_serializers DurationSerializer,
|
185
|
+
SymbolSerializer
|
186
|
+
# The full set of 6 serializers that Rails 6.0 normally adds here -- feel free to include any others if you wish:
|
187
|
+
# SymbolSerializer,
|
188
|
+
# DurationSerializer, # (The one that we've added above in order to support testing)
|
189
|
+
# DateTimeSerializer,
|
190
|
+
# DateSerializer,
|
191
|
+
# TimeWithZoneSerializer,
|
192
|
+
# TimeSerializer
|
193
|
+
end
|
194
|
+
|
195
|
+
# Is the updated version of perform_enqueued_jobs from Rails 6.0 missing from ActionJob's TestHelper?
|
196
|
+
unless TestHelper.private_instance_methods.include?(:flush_enqueued_jobs)
|
197
|
+
module TestHelper
|
198
|
+
def perform_enqueued_jobs(only: nil, except: nil, queue: nil)
|
199
|
+
return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given?
|
200
|
+
|
201
|
+
super
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def jobs_with(jobs, only: nil, except: nil, queue: nil)
|
207
|
+
validate_option(only: only, except: except)
|
208
|
+
|
209
|
+
jobs.count do |job|
|
210
|
+
job_class = job.fetch(:job)
|
211
|
+
|
212
|
+
if only
|
213
|
+
next false unless filter_as_proc(only).call(job)
|
214
|
+
elsif except
|
215
|
+
next false if filter_as_proc(except).call(job)
|
216
|
+
end
|
217
|
+
|
218
|
+
if queue
|
219
|
+
next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
|
220
|
+
end
|
221
|
+
|
222
|
+
yield job if block_given?
|
223
|
+
|
224
|
+
true
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
|
229
|
+
jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
|
230
|
+
end
|
231
|
+
|
232
|
+
def flush_enqueued_jobs(only: nil, except: nil, queue: nil)
|
233
|
+
enqueued_jobs_with(only: only, except: except, queue: queue) do |payload|
|
234
|
+
instantiate_job(payload).perform_now
|
235
|
+
queue_adapter.performed_jobs << payload
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# The following implements polyfills for Rails < 6.0
|
2
|
+
module ActionCable
|
3
|
+
# If the Rails 6.0 ActionCable::TestHelper is missing then allow it to autoload
|
4
|
+
unless ActionCable.const_defined? "TestHelper"
|
5
|
+
autoload :TestHelper, "rails_6_polyfills/actioncable/test_helper.rb"
|
6
|
+
end
|
7
|
+
# If the Rails 6.0 test SubscriptionAdapter is missing then allow it to autoload
|
8
|
+
unless ActionCable.const_defined? "SubscriptionAdapter::Test"
|
9
|
+
module SubscriptionAdapter
|
10
|
+
autoload :Test, "rails_6_polyfills/actioncable/test_adapter.rb"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# If the Rails 6.0 ActionJob Serializers are missing then load support for them
|
16
|
+
unless Object.const_defined?("ActiveJob::Serializers")
|
17
|
+
require "rails_6_polyfills/activejob/serializers"
|
18
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: noticed
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Oliver
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 5.2.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 5.2.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: http
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
description: Database, browser, realtime ActionCable, Email, SMS, Slack notifications,
|
98
112
|
and more for Rails apps
|
99
113
|
email:
|
@@ -124,11 +138,16 @@ files:
|
|
124
138
|
- lib/noticed/delivery_methods/twilio.rb
|
125
139
|
- lib/noticed/delivery_methods/vonage.rb
|
126
140
|
- lib/noticed/engine.rb
|
141
|
+
- lib/noticed/has_notifications.rb
|
127
142
|
- lib/noticed/model.rb
|
128
143
|
- lib/noticed/notification_channel.rb
|
129
144
|
- lib/noticed/text_coder.rb
|
130
145
|
- lib/noticed/translation.rb
|
131
146
|
- lib/noticed/version.rb
|
147
|
+
- lib/rails_6_polyfills/actioncable/test_adapter.rb
|
148
|
+
- lib/rails_6_polyfills/actioncable/test_helper.rb
|
149
|
+
- lib/rails_6_polyfills/activejob/serializers.rb
|
150
|
+
- lib/rails_6_polyfills/base.rb
|
132
151
|
- lib/tasks/noticed_tasks.rake
|
133
152
|
homepage: https://github.com/excid3/noticed
|
134
153
|
licenses:
|