noticed 1.2.17 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5dc2e5f50621a5b5da4f901c472621c2fce03d5f0f4db7c5a00a42de527e3128
4
- data.tar.gz: 1e07d7ea3599b7999d9a252a062ab38641cf6553c8392bf54d70ec9fbc164050
3
+ metadata.gz: 83635d722a21df622c90c4e8ded293206242be768b30b2139c41f13bc4d6712c
4
+ data.tar.gz: 707ff4e0e8d09ed4c1fa1b0a7228353de637b070e849e7eb234b58b6fb491e40
5
5
  SHA512:
6
- metadata.gz: 5f2dc0a1735bea8161241f7b384f722fd0a0d5bf1bac80424d63647d39cf9ef727839f3ba8320474c57da327e381daa46c533a788ca7d3ea1978c5eee10dcbfc
7
- data.tar.gz: 44eb3c5472b947d416d6f14bb2fa1b6a6d9ed26e853eb5d5ca559c0467c0dea9b83634fb1aac72a691b8fbd7805103e3029562e0800d0f433d2ecd9ab5a4e4d1
6
+ metadata.gz: 85ddb6e11858df2b21b4dc098096c78465f7ac3dcb7a804037f3cf1855735b954cccb1cb0ad3227d1ebd89cb98e2e6e5d0138af27e73e52d2feeb35ac85ccdec
7
+ data.tar.gz: 80d3485a086e8203397f83bf3269e057ec63c9da89b3016d29a8395499447419336d89e7d01daff911a838159b963423dd45f6161b80c0dfab608f2f4e11c662
data/README.md CHANGED
@@ -106,6 +106,7 @@ end
106
106
 
107
107
  * `if: :method_name` - Calls `method_name`and cancels delivery method if `false` is returned
108
108
  * `unless: :method_name` - Calls `method_name`and cancels delivery method if `true` is returned
109
+ * `delay: ActiveSupport::Duration` - Delays the delivery for the given duration of time
109
110
 
110
111
  ##### Helper Methods
111
112
 
@@ -186,7 +187,7 @@ Writes notification to the database.
186
187
 
187
188
  `deliver_by :database`
188
189
 
189
- **Note:** Database notifications are special in that they will run before the other delivery methods. We do this so you can reference the database record ID in other delivery methods.
190
+ **Note:** Database notifications are special in that they will run before the other delivery methods. We do this so you can reference the database record ID in other delivery methods. For that same reason, the delivery can't be delayed (via the `delay` option) or an error will be raised.
190
191
 
191
192
  ##### Options
192
193
 
@@ -264,7 +265,7 @@ Sends a Teams notification via webhook.
264
265
 
265
266
  * `format: :format_for_teams` - *Optional*
266
267
 
267
- Use a custom method to define the payload sent to slack. Method should return a Hash.
268
+ Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
268
269
  Documentation for posting via Webhooks available at: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
269
270
 
270
271
  ```ruby
@@ -353,6 +354,33 @@ Sends an SMS notification via Vonage / Nexmo.
353
354
  }
354
355
  ```
355
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
+
356
384
  ### 🚚 Custom Delivery Methods
357
385
 
358
386
  To generate a custom delivery method, simply run
@@ -463,10 +491,19 @@ Sorting notifications by newest first:
463
491
  user.notifications.newest_first
464
492
  ```
465
493
 
466
- Marking all notifications as read:
494
+ Query for read or unread notifications:
495
+
496
+ ```ruby
497
+ user.notifications.read
498
+ user.notifications.unread
499
+ ```
500
+
501
+
502
+ Marking all notifications as read or unread:
467
503
 
468
504
  ```ruby
469
505
  user.notifications.mark_as_read!
506
+ user.notifications.mark_as_unread!
470
507
  ```
471
508
 
472
509
  #### Instance methods
@@ -497,53 +534,43 @@ Adding notification associations to your models makes querying and deleting noti
497
534
 
498
535
  For example, in most cases, you'll want to delete notifications for records that are destroyed.
499
536
 
500
- ##### JSON Columns
537
+ We'll need two associations for this:
501
538
 
502
- If you're using MySQL or Postgresql, the `params` column on the notifications table is in `json` or `jsonb` format and can be queried against directly.
539
+ 1. Notifications where the record is the recipient
540
+ 2. Notifications where the record is in the notification params
503
541
 
504
542
  For example, we can query the notifications and delete them on destroy like so:
505
543
 
506
544
  ```ruby
507
545
  class Post < ApplicationRecord
508
- def notifications
509
- # Exact match
510
- @notifications ||= Notification.where(params: { post: self })
511
-
512
- # Or Postgres syntax to query the post key in the JSON column
513
- # @notifications ||= Notification.where("params->'post' = ?", Noticed::Coder.dump(self).to_json)
514
- end
546
+ # Standard association for deleting notifications when you're the recipient
547
+ has_many :notifications, as: :recipient, dependent: :destroy
515
548
 
516
- before_destroy :destroy_notifications
549
+ # Helper for associating and destroying Notification records where(params: {post: self})
550
+ has_noticed_notifications
517
551
 
518
- def destroy_notifications
519
- notifications.destroy_all
520
- 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"
521
554
  end
522
- ```
523
-
524
- ##### Polymorphic Assocation
525
555
 
526
- If your notification is only associated with one model or you're using a `text` column for your params column , then a polymorphic association is what you'll want to use.
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
527
560
 
528
- 1. Add a polymorphic association to the Notification model. `rails g migration AddNotifiableToNotifications notifiable:belongs_to{polymorphic}`
529
-
530
- 2. Add `has_many :notifications, as: :notifiable, dependent: :destroy` to each model
561
+ CommentNotification.with(parent: @post).deliver(user)
562
+ @post.notifications_as_parent
563
+ ```
531
564
 
532
- 3. Customize database `format: ` option to write the `notifiable` attribute(s) when saving the notification
565
+ #### Handling Deleted Records
533
566
 
534
- ```ruby
535
- class ExampleNotification < Noticed::Base
536
- 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`:
537
568
 
538
- def format_for_database
539
- {
540
- notifiable: params.delete(:post),
541
- type: self.class.name,
542
- params: params
543
- }
544
- end
545
- end
546
- ```
569
+ ```ruby
570
+ class ApplicationJob < ActiveJob::Base
571
+ discard_on ActiveJob::DeserializationError
572
+ end
573
+ ```
547
574
 
548
575
  ## 🙏 Contributing
549
576
 
@@ -43,12 +43,11 @@ module Noticed
43
43
 
44
44
  def params_column
45
45
  case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
- when "mysql2"
47
- "params:json"
48
46
  when "postgresql"
49
47
  "params:jsonb"
50
48
  else
51
- "params:text"
49
+ # MySQL and SQLite both support json
50
+ "params:json"
52
51
  end
53
52
  end
54
53
  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)
@@ -85,9 +87,6 @@ module Noticed
85
87
 
86
88
  # Actually runs an individual delivery
87
89
  def run_delivery_method(delivery_method, recipient:, enqueue:)
88
- return if (delivery_method_name = delivery_method.dig(:options, :if)) && !send(delivery_method_name)
89
- return if (delivery_method_name = delivery_method.dig(:options, :unless)) && send(delivery_method_name)
90
-
91
90
  args = {
92
91
  notification_class: self.class.name,
93
92
  options: delivery_method[:options],
@@ -98,7 +97,15 @@ module Noticed
98
97
 
99
98
  run_callbacks delivery_method[:name] do
100
99
  method = delivery_method_for(delivery_method[:name], delivery_method[:options])
101
- enqueue ? method.perform_later(args) : method.perform_now(args)
100
+
101
+ # Always perfrom later if a delay is present
102
+ if (delay = delivery_method.dig(:options, :delay))
103
+ method.set(wait: delay).perform_later(args)
104
+ elsif enqueue
105
+ method.perform_later(args)
106
+ else
107
+ method.perform_now(args)
108
+ end
102
109
  end
103
110
  end
104
111
 
@@ -40,6 +40,9 @@ module Noticed
40
40
  @notification.record = args[:record]
41
41
  @notification.recipient = args[:recipient]
42
42
 
43
+ return if (condition = @options[:if]) && !@notification.send(condition)
44
+ return if (condition = @options[:unless]) && @notification.send(condition)
45
+
43
46
  run_callbacks :deliver do
44
47
  deliver
45
48
  end
@@ -6,6 +6,13 @@ module Noticed
6
6
  recipient.send(association_name).create!(attributes)
7
7
  end
8
8
 
9
+ def self.validate!(options)
10
+ super
11
+
12
+ # Must be executed right away so the other deliveries can access the db record
13
+ raise ArgumentError, "database delivery cannot be delayed" if options.key?(:delay)
14
+ end
15
+
9
16
  private
10
17
 
11
18
  def association_name
@@ -4,7 +4,7 @@ module Noticed
4
4
  option :mailer
5
5
 
6
6
  def deliver
7
- mailer.with(format).send(method.to_sym).deliver_later
7
+ mailer.with(format).send(method.to_sym).deliver_now
8
8
  end
9
9
 
10
10
  private
@@ -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.merge(
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
@@ -7,6 +7,8 @@ module Noticed
7
7
  if !options[:ignore_failure] && status != "0"
8
8
  raise ResponseUnsuccessful.new(response)
9
9
  end
10
+
11
+ response
10
12
  end
11
13
 
12
14
  private
@@ -1,4 +1,9 @@
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
3
8
  end
4
9
  end
@@ -0,0 +1,32 @@
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
+ model = options.fetch(:model_name, "Notification").constantize
19
+
20
+ define_method "notifications_as_#{param_name}" do
21
+ model.where(params: {param_name.to_sym => self})
22
+ end
23
+
24
+ if options.fetch(:destroy, true)
25
+ before_destroy do
26
+ send("notifications_as_#{param_name}").destroy_all
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
data/lib/noticed/model.rb CHANGED
@@ -14,11 +14,15 @@ module Noticed
14
14
  scope :read, -> { where.not(read_at: nil) }
15
15
  end
16
16
 
17
- module ClassMethods
17
+ class_methods do
18
18
  def mark_as_read!
19
19
  update_all(read_at: Time.current, updated_at: Time.current)
20
20
  end
21
21
 
22
+ def mark_as_unread!
23
+ update_all(read_at: nil, updated_at: Time.current)
24
+ end
25
+
22
26
  def noticed_coder
23
27
  case attribute_types["params"].type
24
28
  when :json, :jsonb
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.2.17"
2
+ VERSION = "1.3.0"
3
3
  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.2.17
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-27 00:00:00.000000000 Z
11
+ date: 2021-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -67,7 +67,35 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: mocha
70
+ name: webmock
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: mysql2
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: sqlite3
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
101
  - - ">="
@@ -110,6 +138,7 @@ files:
110
138
  - lib/noticed/delivery_methods/twilio.rb
111
139
  - lib/noticed/delivery_methods/vonage.rb
112
140
  - lib/noticed/engine.rb
141
+ - lib/noticed/has_notifications.rb
113
142
  - lib/noticed/model.rb
114
143
  - lib/noticed/notification_channel.rb
115
144
  - lib/noticed/text_coder.rb
@@ -135,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
164
  - !ruby/object:Gem::Version
136
165
  version: '0'
137
166
  requirements: []
138
- rubygems_version: 3.1.4
167
+ rubygems_version: 3.2.3
139
168
  signing_key:
140
169
  specification_version: 4
141
170
  summary: Notifications for Ruby on Rails applications