noticed 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 534fd7403e6eb6ad98672132d5c2b3ebf6de0eff7ada3a40ad9acc2bea2b7f2b
4
- data.tar.gz: 18973d1ff94fee48cc3daadd7a1e5df9bc104153dcd6ee10d9b5cfe98df832be
3
+ metadata.gz: 83635d722a21df622c90c4e8ded293206242be768b30b2139c41f13bc4d6712c
4
+ data.tar.gz: 707ff4e0e8d09ed4c1fa1b0a7228353de637b070e849e7eb234b58b6fb491e40
5
5
  SHA512:
6
- metadata.gz: 67cc5fe29f00612c81b64f70fd0b72d264d7f9d3486b14d0892a6482ac1ca75f06303d2bfd8aebfa349d145a269295b06e9530105ca2578d85d148ac5665eefb
7
- data.tar.gz: b66fa2922599a8edb40bdddf3a9a2a05debdfde299877140909fcb339895a0c94b2fa30c2a9d693026a4c040fcbd0c2cb1fdacaefcb328c845783694b04c4100
6
+ metadata.gz: 85ddb6e11858df2b21b4dc098096c78465f7ac3dcb7a804037f3cf1855735b954cccb1cb0ad3227d1ebd89cb98e2e6e5d0138af27e73e52d2feeb35ac85ccdec
7
+ data.tar.gz: 80d3485a086e8203397f83bf3269e057ec63c9da89b3016d29a8395499447419336d89e7d01daff911a838159b963423dd45f6161b80c0dfab608f2f4e11c662
data/README.md CHANGED
@@ -4,18 +4,28 @@
4
4
 
5
5
  ### 🎉 Notifications for your Ruby on Rails app.
6
6
 
7
- [![Build Status](https://github.com/excid3/noticed/workflows/Tests/badge.svg)](https://github.com/excid3/noticed/actions)
7
+ [![Build Status](https://github.com/excid3/noticed/workflows/Tests/badge.svg)](https://github.com/excid3/noticed/actions) [![Gem Version](https://badge.fury.io/rb/noticed.svg)](https://badge.fury.io/rb/noticed)
8
8
 
9
9
  Currently, we support these notification delivery methods out of the box:
10
10
 
11
11
  * Database
12
12
  * Email
13
13
  * ActionCable channels
14
+ * Slack
15
+ * Microsoft Teams
14
16
  * Twilio (SMS)
15
17
  * Vonage / Nexmo (SMS)
16
18
 
17
19
  And you can easily add new notification types for any other delivery methods.
18
20
 
21
+ ## 🎬 Screencast
22
+
23
+ <div style="width:50%">
24
+ <a href="https://www.youtube.com/watch?v=Scffi4otlFc"><img src="https://i.imgur.com/UvVKWwD.png" title="How to add Notifications to Rails with Noticed" /></a>
25
+ </div>
26
+
27
+ [Watch Screencast](https://www.youtube.com/watch?v=Scffi4otlFc)
28
+
19
29
  ## 🚀 Installation
20
30
  Run the following command to add Noticed to your Gemfile
21
31
 
@@ -37,6 +47,28 @@ To generate a notification object, simply run:
37
47
 
38
48
  `rails generate noticed:notification CommentNotification`
39
49
 
50
+ #### Sending Notifications
51
+
52
+ To send a notification to a user:
53
+
54
+ ```ruby
55
+ # Instantiate a new notification
56
+ notification = CommentNotification.with(comment: @comment)
57
+
58
+ # Deliver notification in background job
59
+ notification.deliver_later(@comment.post.author)
60
+
61
+ # Deliver notification immediately
62
+ notification.deliver(@comment.post.author)
63
+
64
+ # Deliver notification to multiple recipients
65
+ notification.deliver_later(User.all)
66
+ ```
67
+
68
+ This will instantiate a new notification with the `comment` stored in the notification's params.
69
+
70
+ Each delivery method is able to transform this metadata that's best for the format. For example, the database may simply store the comment so it can be linked when rendering in the navbar. The websocket mechanism may transform this into a browser notification or insert it into the navbar.
71
+
40
72
  #### Notification Objects
41
73
 
42
74
  Notifications inherit from `Noticed::Base`. This provides all their functionality and allows them to be delivered.
@@ -47,11 +79,7 @@ To add delivery methods, simply `include` the module for the delivery methods yo
47
79
  class CommentNotification < Noticed::Base
48
80
  deliver_by :database
49
81
  deliver_by :action_cable
50
- deliver_by :email, if: :email_notifications?
51
-
52
- def email_notifications?
53
- !!recipient.preferences[:email]
54
- end
82
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
55
83
 
56
84
  # I18n helpers
57
85
  def message
@@ -59,41 +87,26 @@ class CommentNotification < Noticed::Base
59
87
  end
60
88
 
61
89
  # URL helpers are accessible in notifications
90
+ # Don't forget to set your default_url_options so Rails knows how to generate urls
62
91
  def url
63
92
  post_path(params[:post])
64
93
  end
65
94
 
95
+ def email_notifications?
96
+ !!recipient.preferences[:email]
97
+ end
98
+
66
99
  after_deliver do
67
100
  # Anything you want
68
101
  end
69
102
  end
70
103
  ```
71
104
 
72
- #### Sending Notifications
73
-
74
- To send a notification to a user:
75
-
76
- ```ruby
77
- notification = CommentNotification.with(comment: @comment.to_gid)
78
-
79
- # Deliver notification in background job
80
- notification.deliver_later(@comment.post.author)
81
-
82
- # Deliver notification immediately
83
- notification.deliver(@comment.post.author)
84
-
85
- # Deliver notification to multiple recipients
86
- notification.deliver_later(User.all)
87
- ```
88
-
89
- This will instantiate a new notification with the `comment` stored in the notification's params.
90
-
91
- Each delivery method is able to transform this metadata that's best for the format. For example, the database may simply store the comment so it can be linked when rendering in the navbar. The websocket mechanism may transform this into a browser notification or insert it into the navbar.
92
-
93
105
  **Shared Options**
94
106
 
95
107
  * `if: :method_name` - Calls `method_name`and cancels delivery method if `false` is returned
96
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
97
110
 
98
111
  ##### Helper Methods
99
112
 
@@ -103,6 +116,12 @@ You can define helper methods inside your Notification object to make it easier
103
116
 
104
117
  Rails url helpers are included in notification classes by default so you have full access to them just like you would in your controllers and views.
105
118
 
119
+ Don't forget, you'll need to configure `default_url_options` in order for Rails to know what host and port to use when generating URLs.
120
+
121
+ ```ruby
122
+ Rails.application.routes.default_url_options[:host] = 'localhost:3000'
123
+ ```
124
+
106
125
  **Callbacks**
107
126
 
108
127
  Like ActiveRecord, notifications have several different types of callbacks.
@@ -110,7 +129,7 @@ Like ActiveRecord, notifications have several different types of callbacks.
110
129
  ```ruby
111
130
  class CommentNotification < Noticed::Base
112
131
  deliver_by :database
113
- deliver_by :email
132
+ deliver_by :email, mailer: 'CommentMailer'
114
133
 
115
134
  # Callbacks for the entire delivery
116
135
  before_deliver :whatever
@@ -134,7 +153,7 @@ Defining custom delivery methods allows you to add callbacks that run inside the
134
153
 
135
154
  ##### Translations
136
155
 
137
- We've added `translate` and `t` helpers like Rails has to provide an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under `notifications` and the underscored name of the notification class it is used in.
156
+ We've added `translate` and `t` helpers like Rails has to provide an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under `notifications` and the underscored name of the notification class it is used in.
138
157
 
139
158
  For example:
140
159
 
@@ -148,7 +167,7 @@ For example:
148
167
 
149
168
  ```ruby
150
169
  class CommentNotification < Noticed::Base
151
- deliver_by :email, if: :email_notifications?
170
+ deliver_by :email, mailer: 'CommentMailer', if: :email_notifications?
152
171
 
153
172
  def email_notifications?
154
173
  recipient.email_notifications?
@@ -168,7 +187,7 @@ Writes notification to the database.
168
187
 
169
188
  `deliver_by :database`
170
189
 
171
- **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.
172
191
 
173
192
  ##### Options
174
193
 
@@ -176,6 +195,10 @@ Writes notification to the database.
176
195
 
177
196
  The name of the database association to use. Defaults to `:notifications`
178
197
 
198
+ * `format: :format_for_database` - *Optional*
199
+
200
+ Use a custom method to define the attributes saved to the database
201
+
179
202
  ### Email
180
203
 
181
204
  Sends an email notification. Emails will always be sent with `deliver_later`
@@ -192,6 +215,10 @@ Sends an email notification. Emails will always be sent with `deliver_later`
192
215
 
193
216
  Used to customize the method on the mailer that is called
194
217
 
218
+ * `format: :format_for_email` - *Optional*
219
+
220
+ Use a custom method to define the params sent to the mailer. `recipient` will be merged into the params.
221
+
195
222
  ### ActionCable
196
223
 
197
224
  Sends a notification to the browser via websockets (ActionCable channel by default).
@@ -228,6 +255,42 @@ Sends a Slack notification via webhook.
228
255
 
229
256
  Defaults to `Rails.application.credentials.slack[:notification_url]`
230
257
 
258
+ ### Microsoft Teams
259
+
260
+ Sends a Teams notification via webhook.
261
+
262
+ `deliver_by :microsoft_teams`
263
+
264
+ #### Options
265
+
266
+ * `format: :format_for_teams` - *Optional*
267
+
268
+ Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
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
+
271
+ ```ruby
272
+ {
273
+ title: "This is the title for the card",
274
+ text: "This is the body text for the card",
275
+ sections: [{activityTitle: "Section Title", activityText: "Section Text"}],
276
+ "potentialAction": [{
277
+ "@type": "OpenUri",
278
+ name: "Button Text",
279
+ targets: [{
280
+ os: "default",
281
+ uri: "https://example.com/foo/action"
282
+ }]
283
+ }]
284
+
285
+ }
286
+ ```
287
+
288
+ * `url: :url_for_teams_channel`: - *Optional*
289
+
290
+ Use a custom method to retrieve the MS Teams Webhook URL. Method should return a string.
291
+
292
+ Defaults to `Rails.application.credentials.microsoft_teams[:notification_url]`
293
+
231
294
  ### Twilio SMS
232
295
 
233
296
  Sends an SMS notification via Twilio.
@@ -238,7 +301,7 @@ Sends an SMS notification via Twilio.
238
301
 
239
302
  * `credentials: :get_twilio_credentials` - *Optional*
240
303
 
241
- Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_ number` keys.
304
+ Use a custom method to retrieve the credentials for Twilio. Method should return a Hash with `:account_sid`, `:auth_token` and `:phone_number` keys.
242
305
 
243
306
  Defaults to `Rails.application.credentials.twilio[:account_sid]` and `Rails.application.credentials.twilio[:auth_token]`
244
307
 
@@ -264,7 +327,7 @@ Sends an SMS notification via Twilio.
264
327
 
265
328
  ### Vonage SMS
266
329
 
267
- Sends an SMS notification vai Vonage / Nexmo.
330
+ Sends an SMS notification via Vonage / Nexmo.
268
331
 
269
332
  `deliver_by :vonage`
270
333
 
@@ -291,24 +354,57 @@ Sends an SMS notification vai Vonage / Nexmo.
291
354
  }
292
355
  ```
293
356
 
294
- ### 🚚 Custom Delivery Methods
357
+ ### Fallback Notifications
295
358
 
296
- You can define a custom delivery method easily by adding a `deliver_by` line with a unique name and class option. The class will be instantiated and should inherit from `Noticed::DeliveryMethods::Base`.
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.
297
360
 
298
361
  ```ruby
299
- class MyNotification < Noticed::Base
300
- deliver_by :discord, class: "DiscordNotification"
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?
301
377
  end
302
378
  ```
303
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
+
384
+ ### 🚚 Custom Delivery Methods
385
+
386
+ To generate a custom delivery method, simply run
387
+
388
+ `rails generate noticed:delivery_method Discord`
389
+
390
+ This will generate a new `DeliveryMethods::Discord` class inside the `app/notifications/delivery_methods` folder, which can be used to deliver notifications to Discord.
391
+
304
392
  ```ruby
305
- class DiscordNotification < Noticed::DeliveryMethods::Base
393
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
306
394
  def deliver
307
395
  # Logic for sending a Discord notification
308
396
  end
309
397
  end
310
398
  ```
311
399
 
400
+ You can use the custom delivery method thus created by adding a `deliver_by` line with a unique name and `class` option in your notification class.
401
+
402
+ ```ruby
403
+ class MyNotification < Noticed::Base
404
+ deliver_by :discord, class: "DeliveryMethods::Discord"
405
+ end
406
+ ```
407
+
312
408
  Delivery methods have access to the following methods and attributes:
313
409
 
314
410
  * `notification` - The instance of the Notification. You can call methods on the notification to let the user easily override formatting and other functionality of the delivery method.
@@ -316,12 +412,51 @@ Delivery methods have access to the following methods and attributes:
316
412
  * `recipient` - The object who should receive the notification. This is typically a User, Account, or other ActiveRecord model.
317
413
  * `params` - The params passed into the notification. This is details about the event that happened. For example, a user commenting on a post would have params of `{ user: User.first }`
318
414
 
415
+ #### Validating options passed to Custom Delivery methods
416
+
417
+ The presence of the delivery method options is automatically validated if using the `option(s)` method.
418
+
419
+ If you want to validate that the passed options contain valid values, or to add any custom validations, override the `self.validate!(delivery_method_options)` method from the `Noticed::DeliveryMethods::Base` class.
420
+
421
+ ```ruby
422
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
423
+ option :username # Requires the username option to be passed
424
+
425
+ def deliver
426
+ # Logic for sending a Discord notification
427
+ end
428
+
429
+ def self.validate!(delivery_method_options)
430
+ super # Don't forget to call super, otherwise option presence won't be validated
431
+
432
+   # Custom validations
433
+ if delivery_method_options[:username].blank?
434
+ raise Noticed::ValidationError, 'the `username` option must be present'
435
+ end
436
+ end
437
+ end
438
+
439
+ class CommentNotification < Noticed::Base
440
+ deliver_by :discord, class: 'DeliveryMethods::Discord'
441
+ end
442
+ ```
443
+
444
+ Now it will raise an error because a required argument is missing.
445
+
446
+ To fix the error, the argument has to be passed correctly. For example:
447
+
448
+ ```ruby
449
+ class CommentNotification < Noticed::Base
450
+ deliver_by :discord, class: 'DeliveryMethods::Discord', username: User.admin.username
451
+ end
452
+ ```
453
+
319
454
  #### Callbacks
320
455
 
321
456
  Callbacks for delivery methods wrap the *actual* delivery of the notification. You can use `before_deliver`, `around_deliver` and `after_deliver` in your custom delivery methods.
322
457
 
323
458
  ```ruby
324
- class DiscordNotification < Noticed::DeliveryMethods::Base
459
+ class DeliveryMethods::Discord < Noticed::DeliveryMethods::Base
325
460
  after_deliver do
326
461
  # Do whatever you want
327
462
  end
@@ -333,17 +468,110 @@ end
333
468
  Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The following syntax should work for Rails 6.1+:
334
469
 
335
470
  ```ruby
336
- deliver_by DiscordNotification
471
+ deliver_by DeliveryMethods::Discord
337
472
  ```
338
473
 
339
- For Rails 6.0 and earlier, you must pass strings of the class names in the `deliver_by` options.
474
+ For Rails 6.0, you must pass strings of the class names in the `deliver_by` options.
340
475
 
341
476
  ```ruby
342
- deliver_by :discord, class: "DiscordNotification"
477
+ deliver_by :discord, class: "DeliveryMethods::Discord"
343
478
  ```
344
479
 
345
480
  We recommend the Rails 6.0 compatible options to prevent confusion.
346
481
 
482
+ ### 📦 Database Model
483
+
484
+ The Notification database model includes several helpful features to make working with database notifications easier.
485
+
486
+ #### Class methods
487
+
488
+ Sorting notifications by newest first:
489
+
490
+ ```ruby
491
+ user.notifications.newest_first
492
+ ```
493
+
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:
503
+
504
+ ```ruby
505
+ user.notifications.mark_as_read!
506
+ user.notifications.mark_as_unread!
507
+ ```
508
+
509
+ #### Instance methods
510
+
511
+ Convert back into a Noticed notification object:
512
+
513
+ ```ruby
514
+ @notification.to_notification
515
+ ```
516
+
517
+ Mark notification as read / unread:
518
+
519
+ ```ruby
520
+ @notification.mark_as_read!
521
+ @notification.mark_as_unread!
522
+ ```
523
+
524
+ Check if read / unread:
525
+
526
+ ```ruby
527
+ @notification.read?
528
+ @notification.unread?
529
+ ```
530
+
531
+ #### Associating Notifications
532
+
533
+ Adding notification associations to your models makes querying and deleting notifications easy and is a pretty critical feature of most applications.
534
+
535
+ For example, in most cases, you'll want to delete notifications for records that are destroyed.
536
+
537
+ We'll need two associations for this:
538
+
539
+ 1. Notifications where the record is the recipient
540
+ 2. Notifications where the record is in the notification params
541
+
542
+ For example, we can query the notifications and delete them on destroy like so:
543
+
544
+ ```ruby
545
+ class Post < ApplicationRecord
546
+ # Standard association for deleting notifications when you're the recipient
547
+ has_many :notifications, as: :recipient, dependent: :destroy
548
+
549
+ # Helper for associating and destroying Notification records where(params: {post: self})
550
+ has_noticed_notifications
551
+
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"
554
+ end
555
+
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
560
+
561
+ CommentNotification.with(parent: @post).deliver(user)
562
+ @post.notifications_as_parent
563
+ ```
564
+
565
+ #### Handling Deleted Records
566
+
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`:
568
+
569
+ ```ruby
570
+ class ApplicationJob < ActiveJob::Base
571
+ discard_on ActiveJob::DeserializationError
572
+ end
573
+ ```
574
+
347
575
  ## 🙏 Contributing
348
576
 
349
577
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
data/Rakefile CHANGED
@@ -18,6 +18,10 @@ require "bundler/gem_tasks"
18
18
 
19
19
  require "rake/testtask"
20
20
 
21
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
22
+ load "rails/tasks/engine.rake"
23
+ load "rails/tasks/statistics.rake"
24
+
21
25
  Rake::TestTask.new(:test) do |t|
22
26
  t.libs << "test"
23
27
  t.pattern = "test/**/*_test.rb"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Noticed
6
+ module Generators
7
+ class DeliveryMethodGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a class for a custom delivery method with the given NAME."
13
+
14
+ def generate_notification
15
+ template "delivery_method.rb", "app/notifications/delivery_methods/#{singular_name}.rb"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,13 +15,22 @@ module Noticed
15
15
  argument :attributes, type: :array, default: [], banner: "field:type field:type"
16
16
 
17
17
  def generate_notification
18
- generate :model, name, "recipient:references{polymorphic}", "type", "params:text", "read_at:datetime", *attributes
18
+ generate :model, name, "recipient:references{polymorphic}", "type", params_column, "read_at:datetime:index", *attributes
19
19
  end
20
20
 
21
21
  def add_noticed_model
22
22
  inject_into_class model_path, class_name, " include Noticed::Model\n"
23
23
  end
24
24
 
25
+ def add_not_nullable
26
+ migration_path = Dir.glob(Rails.root.join("db/migrate/*")).max_by { |f| File.mtime(f) }
27
+
28
+ # Force is required because null: false already exists in the file and Thor isn't smart enough to tell the difference
29
+ insert_into_file migration_path, after: "t.string :type", force: true do
30
+ ", null: false"
31
+ end
32
+ end
33
+
25
34
  def done
26
35
  readme "README" if behavior == :invoke
27
36
  end
@@ -31,6 +40,16 @@ module Noticed
31
40
  def model_path
32
41
  @model_path ||= File.join("app", "models", "#{file_path}.rb")
33
42
  end
43
+
44
+ def params_column
45
+ case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
+ when "postgresql"
47
+ "params:jsonb"
48
+ else
49
+ # MySQL and SQLite both support json
50
+ "params:json"
51
+ end
52
+ end
34
53
  end
35
54
  end
36
55
  end
@@ -12,7 +12,7 @@ module Noticed
12
12
  desc "Generates a notification with the given NAME."
13
13
 
14
14
  def generate_notification
15
- template "notification.rb", "app/notifications/#{singular_name}.rb"
15
+ template "notification.rb", "app/notifications/#{file_path}.rb"
16
16
  end
17
17
  end
18
18
  end
@@ -0,0 +1,12 @@
1
+ class DeliveryMethods::<%= class_name %> < Noticed::DeliveryMethods::Base
2
+ def deliver
3
+ # Logic for sending the notification
4
+ end
5
+
6
+ # You may override this method to validate options for the delivery method
7
+ # Invalid options should raise a ValidationError
8
+ #
9
+ # def self.validate!(options)
10
+ # raise ValidationError, "required_option missing" unless options[:required_option]
11
+ # end
12
+ end
@@ -22,6 +22,6 @@ class <%= class_name %> < Noticed::Base
22
22
  # end
23
23
  #
24
24
  # def url
25
- # posts_path(params[:post])
25
+ # post_path(params[:post])
26
26
  # end
27
27
  end
data/lib/noticed/base.rb CHANGED
@@ -9,7 +9,10 @@ module Noticed
9
9
  class_attribute :delivery_methods, instance_writer: false, default: []
10
10
  class_attribute :param_names, instance_writer: false, default: []
11
11
 
12
- attr_accessor :record
12
+ # Gives notifications access to the record and recipient when formatting for delivery
13
+ attr_accessor :record, :recipient
14
+
15
+ delegate :read?, :unread?, to: :record
13
16
 
14
17
  class << self
15
18
  def deliver_by(name, options = {})
@@ -28,9 +31,10 @@ module Noticed
28
31
  new(params)
29
32
  end
30
33
 
31
- def param(name)
32
- param_names.push(name)
34
+ def params(*names)
35
+ param_names.concat Array.wrap(names)
33
36
  end
37
+ alias_method :param, :params
34
38
  end
35
39
 
36
40
  def initialize(params = {})
@@ -67,6 +71,9 @@ module Noticed
67
71
  def run_delivery(recipient, enqueue: true)
68
72
  delivery_methods = self.class.delivery_methods.dup
69
73
 
74
+ # Set recipient to instance var so it is available to Notification class
75
+ @recipient = recipient
76
+
70
77
  # Run database delivery inline first if it exists so other methods have access to the record
71
78
  if (index = delivery_methods.find_index { |m| m[:name] == :database })
72
79
  delivery_method = delivery_methods.delete_at(index)
@@ -80,9 +87,6 @@ module Noticed
80
87
 
81
88
  # Actually runs an individual delivery
82
89
  def run_delivery_method(delivery_method, recipient:, enqueue:)
83
- return if (delivery_method_name = delivery_method.dig(:options, :if)) && !send(delivery_method_name)
84
- return if (delivery_method_name = delivery_method.dig(:options, :unless)) && send(delivery_method_name)
85
-
86
90
  args = {
87
91
  notification_class: self.class.name,
88
92
  options: delivery_method[:options],
@@ -92,27 +96,46 @@ module Noticed
92
96
  }
93
97
 
94
98
  run_callbacks delivery_method[:name] do
95
- klass = get_class(delivery_method[:name], delivery_method[:options])
96
- enqueue ? klass.perform_later(args) : klass.perform_now(args)
99
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
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
97
109
  end
98
110
  end
99
111
 
100
- # Retrieves the correct class for a delivery method
101
- def get_class(name, options)
112
+ def delivery_method_for(name, options)
102
113
  if options[:class]
103
114
  options[:class].constantize
104
115
  else
105
- "Noticed::DeliveryMethods::#{name.to_s.classify}".constantize
116
+ "Noticed::DeliveryMethods::#{name.to_s.camelize}".constantize
106
117
  end
107
118
  end
108
119
 
109
- # Validates that all params are present
110
120
  def validate!
121
+ validate_params_present!
122
+ validate_options_of_delivery_methods!
123
+ end
124
+
125
+ # Validates that all params are present
126
+ def validate_params_present!
111
127
  self.class.param_names.each do |param_name|
112
128
  if params[param_name].nil?
113
129
  raise ValidationError, "#{param_name} is missing."
114
130
  end
115
131
  end
116
132
  end
133
+
134
+ def validate_options_of_delivery_methods!
135
+ delivery_methods.each do |delivery_method|
136
+ method = delivery_method_for(delivery_method[:name], delivery_method[:options])
137
+ method.validate!(delivery_method[:options])
138
+ end
139
+ end
117
140
  end
118
141
  end
data/lib/noticed/coder.rb CHANGED
@@ -1,10 +1,12 @@
1
1
  module Noticed
2
2
  class Coder
3
3
  def self.load(data)
4
+ return if data.nil?
4
5
  ActiveJob::Arguments.send(:deserialize_argument, data)
5
6
  end
6
7
 
7
8
  def self.dump(data)
9
+ return if data.nil?
8
10
  ActiveJob::Arguments.send(:serialize_argument, data)
9
11
  end
10
12
  end
@@ -16,10 +16,18 @@ module Noticed
16
16
  end
17
17
 
18
18
  def channel
19
- if (method = options[:channel])
20
- notification.send(method)
21
- else
22
- Noticed::NotificationChannel
19
+ @channel ||= begin
20
+ value = options[:channel]
21
+ case value
22
+ when String
23
+ value.constantize
24
+ when Symbol
25
+ notification.send(value)
26
+ when Class
27
+ value
28
+ else
29
+ Noticed::NotificationChannel
30
+ end
23
31
  end
24
32
  end
25
33
  end
@@ -4,15 +4,44 @@ module Noticed
4
4
  extend ActiveModel::Callbacks
5
5
  define_model_callbacks :deliver
6
6
 
7
- attr_reader :notification, :options, :recipient
7
+ class_attribute :option_names, instance_writer: false, default: []
8
8
 
9
- def perform(notification_class:, options:, params:, recipient:, record:)
10
- @notification = notification_class.constantize.new(params)
11
- @options = options
12
- @recipient = recipient
9
+ attr_reader :notification, :options, :params, :recipient, :record
13
10
 
14
- # Keep track of the database record for rendering
15
- @notification.record = record
11
+ class << self
12
+ # Copy option names from parent
13
+ def inherited(base) #:nodoc:
14
+ base.option_names = option_names.dup
15
+ super
16
+ end
17
+
18
+ def options(*names)
19
+ option_names.concat Array.wrap(names)
20
+ end
21
+ alias_method :option, :options
22
+
23
+ def validate!(delivery_method_options)
24
+ option_names.each do |option_name|
25
+ unless delivery_method_options.key? option_name
26
+ raise ValidationError, "option `#{option_name}` must be set for #{name}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def perform(args)
33
+ @notification = args[:notification_class].constantize.new(args[:params])
34
+ @options = args[:options]
35
+ @params = args[:params]
36
+ @recipient = args[:recipient]
37
+ @record = args[:record]
38
+
39
+ # Make notification aware of database record and recipient during delivery
40
+ @notification.record = args[:record]
41
+ @notification.recipient = args[:recipient]
42
+
43
+ return if (condition = @options[:if]) && !@notification.send(condition)
44
+ return if (condition = @options[:unless]) && @notification.send(condition)
16
45
 
17
46
  run_callbacks :deliver do
18
47
  deliver
@@ -22,6 +51,36 @@ module Noticed
22
51
  def deliver
23
52
  raise NotImplementedError, "Delivery methods must implement a deliver method"
24
53
  end
54
+
55
+ private
56
+
57
+ # Helper method for making POST requests from delivery methods
58
+ #
59
+ # Usage:
60
+ # post("http://example.com", basic_auth: {user:, pass:}, json: {}, form: {})
61
+ #
62
+ def post(url, args = {})
63
+ basic_auth = args.delete(:basic_auth)
64
+
65
+ request = if basic_auth
66
+ HTTP.basic_auth(user: basic_auth[:user], pass: basic_auth[:pass])
67
+ else
68
+ HTTP
69
+ end
70
+
71
+ response = request.post(url, args)
72
+
73
+ if options[:debug]
74
+ Rails.logger.debug("POST #{url}")
75
+ Rails.logger.debug("Response: #{response.code}: #{response}")
76
+ end
77
+
78
+ if !options[:ignore_failure] && !response.status.success?
79
+ raise ResponseUnsuccessful.new(response)
80
+ end
81
+
82
+ response
83
+ end
25
84
  end
26
85
  end
27
86
  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
@@ -1,8 +1,10 @@
1
1
  module Noticed
2
2
  module DeliveryMethods
3
3
  class Email < Base
4
+ option :mailer
5
+
4
6
  def deliver
5
- mailer.with(notification.params).send(method.to_sym).deliver_later
7
+ mailer.with(format).send(method.to_sym).deliver_now
6
8
  end
7
9
 
8
10
  private
@@ -14,6 +16,15 @@ module Noticed
14
16
  def method
15
17
  options[:method] || notification.class.name.underscore
16
18
  end
19
+
20
+ def format
21
+ params = if (method = options[:format])
22
+ notification.send(method)
23
+ else
24
+ notification.params
25
+ end
26
+ params.merge(recipient: recipient, record: record)
27
+ end
17
28
  end
18
29
  end
19
30
  end
@@ -0,0 +1,32 @@
1
+ module Noticed
2
+ module DeliveryMethods
3
+ class MicrosoftTeams < Base
4
+ def deliver
5
+ post(url, json: format)
6
+ end
7
+
8
+ private
9
+
10
+ def format
11
+ if (method = options[:format])
12
+ notification.send(method)
13
+ else
14
+ {
15
+ title: notification.params[:title],
16
+ text: notification.params[:text],
17
+ sections: notification.params[:sections],
18
+ potentialAction: notification.params[:notification_action]
19
+ }
20
+ end
21
+ end
22
+
23
+ def url
24
+ if (method = options[:url])
25
+ notification.send(method)
26
+ else
27
+ Rails.application.credentials.microsoft_teams[:notification_url]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Slack < Base
4
4
  def deliver
5
- HTTP.post(url, json: format)
5
+ post(url, json: format)
6
6
  end
7
7
 
8
8
  private
@@ -11,7 +11,7 @@ module Noticed
11
11
  if (method = options[:format])
12
12
  notification.send(method)
13
13
  else
14
- params
14
+ notification.params
15
15
  end
16
16
  end
17
17
 
@@ -2,7 +2,7 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Twilio < Base
4
4
  def deliver
5
- HTTP.basic_auth(user: account_sid, pass: auth_token).post(url, json: format)
5
+ post(url, basic_auth: {user: account_sid, pass: auth_token}, form: format)
6
6
  end
7
7
 
8
8
  private
@@ -2,7 +2,13 @@ module Noticed
2
2
  module DeliveryMethods
3
3
  class Vonage < Base
4
4
  def deliver
5
- HTTP.post("https://rest.nexmo.com/sms/json", json: format)
5
+ response = post("https://rest.nexmo.com/sms/json", json: format)
6
+ status = response.parse.dig("messages", 0, "status")
7
+ if !options[:ignore_failure] && status != "0"
8
+ raise ResponseUnsuccessful.new(response)
9
+ end
10
+
11
+ response
6
12
  end
7
13
 
8
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
@@ -5,32 +5,51 @@ module Noticed
5
5
  included do
6
6
  self.inheritance_column = nil
7
7
 
8
- serialize :params, Noticed::Coder
8
+ serialize :params, noticed_coder
9
9
 
10
10
  belongs_to :recipient, polymorphic: true
11
11
 
12
12
  scope :newest_first, -> { order(created_at: :desc) }
13
+ scope :unread, -> { where(read_at: nil) }
14
+ scope :read, -> { where.not(read_at: nil) }
13
15
  end
14
16
 
15
- module ClassMethods
17
+ class_methods do
16
18
  def mark_as_read!
17
19
  update_all(read_at: Time.current, updated_at: Time.current)
18
20
  end
21
+
22
+ def mark_as_unread!
23
+ update_all(read_at: nil, updated_at: Time.current)
24
+ end
25
+
26
+ def noticed_coder
27
+ case attribute_types["params"].type
28
+ when :json, :jsonb
29
+ Noticed::Coder
30
+ else
31
+ Noticed::TextCoder
32
+ end
33
+ end
19
34
  end
20
35
 
21
36
  # Rehydrate the database notification into the Notification object for rendering
22
37
  def to_notification
23
38
  @_notification ||= begin
24
- instance = type.constantize.with(params)
25
- instance.record = self
26
- instance
27
- end
39
+ instance = type.constantize.with(params)
40
+ instance.record = self
41
+ instance
42
+ end
28
43
  end
29
44
 
30
45
  def mark_as_read!
31
46
  update(read_at: Time.current)
32
47
  end
33
48
 
49
+ def mark_as_unread!
50
+ update(read_at: nil)
51
+ end
52
+
34
53
  def unread?
35
54
  !read?
36
55
  end
@@ -0,0 +1,16 @@
1
+ module Noticed
2
+ class TextCoder
3
+ def self.load(data)
4
+ return if data.nil?
5
+
6
+ # Text columns need JSON parsing
7
+ data = JSON.parse(data)
8
+ ActiveJob::Arguments.send(:deserialize_argument, data)
9
+ end
10
+
11
+ def self.dump(data)
12
+ return if data.nil?
13
+ ActiveJob::Arguments.send(:serialize_argument, data).to_json
14
+ end
15
+ end
16
+ end
@@ -1,21 +1,23 @@
1
- module Translation
2
- extend ActiveSupport::Concern
1
+ module Noticed
2
+ module Translation
3
+ extend ActiveSupport::Concern
3
4
 
4
- # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
5
- def i18n_scope
6
- :notifications
7
- end
5
+ # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
6
+ def i18n_scope
7
+ :notifications
8
+ end
8
9
 
9
- def translate(key, **options)
10
- I18n.translate(scope_translation_key(key), **options)
11
- end
12
- alias t translate
10
+ def translate(key, **options)
11
+ I18n.translate(scope_translation_key(key), **options)
12
+ end
13
+ alias_method :t, :translate
13
14
 
14
- def scope_translation_key(key)
15
- if key.to_s.start_with?(".")
16
- "notifications.#{self.class.name.underscore}#{key}"
17
- else
18
- key
15
+ def scope_translation_key(key)
16
+ if key.to_s.start_with?(".")
17
+ "#{i18n_scope}.#{self.class.name.underscore}#{key}"
18
+ else
19
+ key
20
+ end
19
21
  end
20
22
  end
21
23
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.2.2"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/noticed.rb CHANGED
@@ -1,11 +1,15 @@
1
+ require "active_job/arguments"
1
2
  require "http"
2
3
  require "noticed/engine"
3
4
 
4
5
  module Noticed
5
6
  autoload :Base, "noticed/base"
6
7
  autoload :Coder, "noticed/coder"
8
+ autoload :HasNotifications, "noticed/has_notifications"
7
9
  autoload :Model, "noticed/model"
10
+ autoload :TextCoder, "noticed/text_coder"
8
11
  autoload :Translation, "noticed/translation"
12
+ autoload :NotificationChannel, "noticed/notification_channel"
9
13
 
10
14
  module DeliveryMethods
11
15
  autoload :Base, "noticed/delivery_methods/base"
@@ -13,6 +17,7 @@ module Noticed
13
17
  autoload :Database, "noticed/delivery_methods/database"
14
18
  autoload :Email, "noticed/delivery_methods/email"
15
19
  autoload :Slack, "noticed/delivery_methods/slack"
20
+ autoload :MicrosoftTeams, "noticed/delivery_methods/microsoft_teams"
16
21
  autoload :Test, "noticed/delivery_methods/test"
17
22
  autoload :Twilio, "noticed/delivery_methods/twilio"
18
23
  autoload :Vonage, "noticed/delivery_methods/vonage"
@@ -32,4 +37,12 @@ module Noticed
32
37
 
33
38
  class ValidationError < StandardError
34
39
  end
40
+
41
+ class ResponseUnsuccessful < StandardError
42
+ attr_reader :response
43
+
44
+ def initialize(response)
45
+ @response = response
46
+ end
47
+ end
35
48
  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.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-03 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
  - - ">="
@@ -91,10 +119,11 @@ files:
91
119
  - MIT-LICENSE
92
120
  - README.md
93
121
  - Rakefile
94
- - app/channels/noticed/notification_channel.rb
122
+ - lib/generators/noticed/delivery_method_generator.rb
95
123
  - lib/generators/noticed/model_generator.rb
96
124
  - lib/generators/noticed/notification_generator.rb
97
125
  - lib/generators/noticed/templates/README
126
+ - lib/generators/noticed/templates/delivery_method.rb.tt
98
127
  - lib/generators/noticed/templates/notification.rb.tt
99
128
  - lib/noticed.rb
100
129
  - lib/noticed/base.rb
@@ -103,12 +132,16 @@ files:
103
132
  - lib/noticed/delivery_methods/base.rb
104
133
  - lib/noticed/delivery_methods/database.rb
105
134
  - lib/noticed/delivery_methods/email.rb
135
+ - lib/noticed/delivery_methods/microsoft_teams.rb
106
136
  - lib/noticed/delivery_methods/slack.rb
107
137
  - lib/noticed/delivery_methods/test.rb
108
138
  - lib/noticed/delivery_methods/twilio.rb
109
139
  - lib/noticed/delivery_methods/vonage.rb
110
140
  - lib/noticed/engine.rb
141
+ - lib/noticed/has_notifications.rb
111
142
  - lib/noticed/model.rb
143
+ - lib/noticed/notification_channel.rb
144
+ - lib/noticed/text_coder.rb
112
145
  - lib/noticed/translation.rb
113
146
  - lib/noticed/version.rb
114
147
  - lib/tasks/noticed_tasks.rake
@@ -116,7 +149,7 @@ homepage: https://github.com/excid3/noticed
116
149
  licenses:
117
150
  - MIT
118
151
  metadata: {}
119
- post_install_message:
152
+ post_install_message:
120
153
  rdoc_options: []
121
154
  require_paths:
122
155
  - lib
@@ -131,8 +164,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
164
  - !ruby/object:Gem::Version
132
165
  version: '0'
133
166
  requirements: []
134
- rubygems_version: 3.1.2
135
- signing_key:
167
+ rubygems_version: 3.2.3
168
+ signing_key:
136
169
  specification_version: 4
137
170
  summary: Notifications for Ruby on Rails applications
138
171
  test_files: []