activity_notification 2.5.1 → 2.6.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/app/channels/activity_notification/notification_api_channel.rb +5 -5
  4. data/app/channels/activity_notification/notification_api_with_devise_channel.rb +4 -4
  5. data/app/channels/activity_notification/notification_channel.rb +4 -0
  6. data/app/channels/activity_notification/notification_with_devise_channel.rb +4 -4
  7. data/app/controllers/activity_notification/notifications_controller.rb +1 -2
  8. data/app/controllers/activity_notification/subscriptions_controller.rb +2 -3
  9. data/app/jobs/activity_notification/notify_all_job.rb +2 -2
  10. data/app/jobs/activity_notification/notify_job.rb +2 -2
  11. data/app/jobs/activity_notification/notify_to_job.rb +1 -1
  12. data/app/mailers/activity_notification/mailer.rb +1 -1
  13. data/app/views/activity_notification/mailer/default/batch_default.text.erb +1 -1
  14. data/app/views/activity_notification/notifications/default/index.html.erb +1 -1
  15. data/app/views/activity_notification/subscriptions/default/_notification_keys.html.erb +1 -1
  16. data/app/views/activity_notification/subscriptions/default/_subscription.html.erb +1 -1
  17. data/docs/Functions.md +93 -9
  18. data/docs/Setup.md +7 -7
  19. data/docs/Testing.md +1 -1
  20. data/docs/Upgrade-to-2.6.md +108 -0
  21. data/lib/activity_notification/apis/notification_api.rb +55 -40
  22. data/lib/activity_notification/apis/subscription_api.rb +10 -10
  23. data/lib/activity_notification/common.rb +5 -5
  24. data/lib/activity_notification/config.rb +15 -5
  25. data/lib/activity_notification/controllers/common_controller.rb +2 -4
  26. data/lib/activity_notification/controllers/devise_authentication_controller.rb +2 -2
  27. data/lib/activity_notification/helpers/polymorphic_helpers.rb +6 -6
  28. data/lib/activity_notification/helpers/view_helpers.rb +3 -3
  29. data/lib/activity_notification/mailers/helpers.rb +88 -2
  30. data/lib/activity_notification/models/concerns/notifiable.rb +60 -30
  31. data/lib/activity_notification/models/concerns/notifier.rb +1 -1
  32. data/lib/activity_notification/models/concerns/subscriber.rb +72 -15
  33. data/lib/activity_notification/models/concerns/target.rb +38 -35
  34. data/lib/activity_notification/optional_targets/action_cable_api_channel.rb +1 -1
  35. data/lib/activity_notification/optional_targets/slack.rb +2 -2
  36. data/lib/activity_notification/orm/active_record/notification.rb +25 -25
  37. data/lib/activity_notification/orm/active_record/subscription.rb +21 -1
  38. data/lib/activity_notification/orm/dynamoid/extension.rb +3 -3
  39. data/lib/activity_notification/orm/dynamoid/subscription.rb +8 -1
  40. data/lib/activity_notification/orm/dynamoid.rb +18 -18
  41. data/lib/activity_notification/orm/mongoid/notification.rb +26 -28
  42. data/lib/activity_notification/orm/mongoid/subscription.rb +21 -1
  43. data/lib/activity_notification/orm/mongoid.rb +1 -1
  44. data/lib/activity_notification/rails/routes.rb +11 -11
  45. data/lib/activity_notification/roles/acts_as_group.rb +1 -1
  46. data/lib/activity_notification/roles/acts_as_notifiable.rb +5 -5
  47. data/lib/activity_notification/roles/acts_as_notifier.rb +1 -1
  48. data/lib/activity_notification/roles/acts_as_target.rb +1 -1
  49. data/lib/activity_notification/version.rb +1 -1
  50. data/lib/generators/activity_notification/add_notifiable_to_subscriptions/add_notifiable_to_subscriptions_generator.rb +23 -0
  51. data/lib/generators/activity_notification/add_notifiable_to_subscriptions/templates/add_notifiable_to_subscriptions.rb +13 -0
  52. data/lib/generators/templates/activity_notification.rb +14 -2
  53. data/lib/generators/templates/migrations/migration.rb +4 -2
  54. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30af4a0429874914d32df8bdff256add7d9278b9f19bbd9c3a03f23138dac654
4
- data.tar.gz: f08926e67ff5a91cfa3e80bc94af31bf4e7d19f2fb1198a75fa2d906a69bc17e
3
+ metadata.gz: ba3fa5f43981ca60a99f7b6ace089f4ceca465187b886b022bfca601df919bac
4
+ data.tar.gz: b09792d6faae2462287d227d2b12ac279bada455c40ab550f4894b373db57481
5
5
  SHA512:
6
- metadata.gz: ea7191de6c42a8898d63462595f3f46dc48f3d3cbc10c54e1fad225fcab8d3bb5be15d4c05015ff800a8e4090e2080f1943fa3d72d956b2b7a5404d6a88fe7b1
7
- data.tar.gz: 85cfb749ccab3170964b270f405d24d6d715dc2d1f808ec95e18a8ce0fd14a30250cf83b3985869996358339b5b2b00807bd9b993599a034271288cecc9914ee
6
+ metadata.gz: bbd0135c886fec12c0a93955fb65ef8d6539b2353c88210e5986d50be428a4a7b03a0c015b23777d47011005889869f5bcb126f1d8a833127e9ef9debe98a47f
7
+ data.tar.gz: ee2f15a533731db9a4d9bc6a65b7d5935f38daa138885806774134da4cdc831b5acb51116c9af1bd83c154b070d83c178d1768a51eb3c27b182171f830e80047
data/README.md CHANGED
@@ -23,10 +23,12 @@
23
23
  * Automatic tracked notifications (generating notifications along with the lifecycle of notifiable models)
24
24
  * Grouping notifications (grouping like *"Kevin and 7 other users posted comments to this article"*)
25
25
  * Email notification
26
+ * Email attachments (configurable at global, target, and notifiable levels)
26
27
  * Batch email notification (event driven or periodical email notification, daily or weekly etc)
27
28
  * Cascading notifications (progressive notification escalation through multiple channels with time delays)
28
29
  * Push notification with [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html)
29
30
  * Subscription management (subscribing and unsubscribing for each target and notification type)
31
+ * Instance-level subscriptions (subscribing to notifications from a specific notifiable instance)
30
32
  * REST API backend and [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification)
31
33
  * Integration with [Devise](https://github.com/plataformatec/devise) authentication
32
34
  * Activity notifications stream integrated into cloud computing using [Amazon DynamoDB Streams](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html)
@@ -1,8 +1,8 @@
1
- if defined?(ActionCable)
2
- # Action Cable API channel to subscribe broadcasted notifications.
3
- class ActivityNotification::NotificationApiChannel < ActivityNotification::NotificationChannel
4
- # ActionCable::Channel::Base#subscribed
5
- # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
1
+ # Action Cable API channel to subscribe broadcasted notifications.
2
+ class ActivityNotification::NotificationApiChannel < ActivityNotification::NotificationChannel
3
+ if defined?(ActionCable)
4
+ # ActionCable::Channel::Base#subscribed
5
+ # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
6
6
  def subscribed
7
7
  stream_from "#{ActivityNotification.config.notification_api_channel_prefix}_#{@target.to_class_name}#{ActivityNotification.config.composite_key_delimiter}#{@target.id}"
8
8
  rescue
@@ -1,6 +1,6 @@
1
- if defined?(ActionCable)
2
- # Action Cable API channel to subscribe broadcasted notifications with Devise authentication.
3
- class ActivityNotification::NotificationApiWithDeviseChannel < ActivityNotification::NotificationApiChannel
1
+ # Action Cable API channel to subscribe broadcasted notifications with Devise authentication.
2
+ class ActivityNotification::NotificationApiWithDeviseChannel < ActivityNotification::NotificationApiChannel
3
+ if defined?(ActionCable)
4
4
  # Include PolymorphicHelpers to resolve string extentions
5
5
  include ActivityNotification::PolymorphicHelpers
6
6
 
@@ -18,7 +18,7 @@ if defined?(ActionCable)
18
18
  end
19
19
 
20
20
  # Set @target instance variable from request parameters.
21
- # This method overrides super (ActivityNotiication::NotificationChannel#set_target)
21
+ # This method overrides super (ActivityNotification::NotificationChannel#set_target)
22
22
  # to set devise authenticated target when the target_id params is not specified.
23
23
  # @api protected
24
24
  # @return [Object] Target instance (Reject subscription when request parameters are not enough)
@@ -34,4 +34,8 @@ if defined?(ActionCable)
34
34
  reject if @target.nil? || @target.notification_action_cable_with_devise?
35
35
  end
36
36
  end
37
+ else
38
+ # :nocov:
39
+ class ActivityNotification::NotificationChannel; end
40
+ # :nocov:
37
41
  end
@@ -1,6 +1,6 @@
1
- if defined?(ActionCable)
2
- # Action Cable channel to subscribe broadcasted notifications with Devise authentication.
3
- class ActivityNotification::NotificationWithDeviseChannel < ActivityNotification::NotificationChannel
1
+ # Action Cable channel to subscribe broadcasted notifications with Devise authentication.
2
+ class ActivityNotification::NotificationWithDeviseChannel < ActivityNotification::NotificationChannel
3
+ if defined?(ActionCable)
4
4
  # Include PolymorphicHelpers to resolve string extentions
5
5
  include ActivityNotification::PolymorphicHelpers
6
6
 
@@ -23,7 +23,7 @@ if defined?(ActionCable)
23
23
  end
24
24
 
25
25
  # Sets @target instance variable from request parameters.
26
- # This method override super (ActivityNotiication::NotificationChannel#set_target)
26
+ # This method override super (ActivityNotification::NotificationChannel#set_target)
27
27
  # to set devise authenticated target when the target_id params is not specified.
28
28
  # @api protected
29
29
  # @return [Object] Target instance (Reject subscription when request parameters are not enough)
@@ -159,8 +159,7 @@ module ActivityNotification
159
159
  limit = params[:limit].to_i > 0 ? params[:limit].to_i : nil
160
160
  reverse = params[:reverse].present? ?
161
161
  params[:reverse].to_s.to_boolean(false) : nil
162
- with_group_members = params[:with_group_members].present? || params[:without_grouping].present? ?
163
- params[:with_group_members].to_s.to_boolean(false) || params[:without_grouping].to_s.to_boolean(false) : nil
162
+ with_group_members = params[:with_group_members].present? || params[:without_grouping].present? ? params[:with_group_members].to_s.to_boolean(false) || params[:without_grouping].to_s.to_boolean(false) : nil
164
163
  @index_options = params.permit(:filter, :filtered_by_type, :filtered_by_group_type, :filtered_by_group_id, :filtered_by_key, :later_than, :earlier_than, :routing_scope, :devise_default_routes)
165
164
  .to_h.symbolize_keys
166
165
  .merge(limit: limit, reverse: reverse, with_group_members: with_group_members)
@@ -191,7 +191,7 @@ module ActivityNotification
191
191
  params[:subscription][:optional_targets][optional_target_key] = boolean_value
192
192
  end
193
193
  end
194
- params.require(:subscription).permit(:key, :subscribing, :subscribing_to_email, optional_targets: optional_target_keys)
194
+ params.require(:subscription).permit(:key, :subscribing, :subscribing_to_email, :notifiable_type, :notifiable_id, optional_targets: optional_target_keys)
195
195
  end
196
196
 
197
197
  # Sets options to load subscription index from request parameters.
@@ -199,8 +199,7 @@ module ActivityNotification
199
199
  # @return [Hash] options to load subscription index
200
200
  def set_index_options
201
201
  limit = params[:limit].to_i > 0 ? params[:limit].to_i : nil
202
- reverse = params[:reverse].present? ?
203
- params[:reverse].to_s.to_boolean(false) : nil
202
+ reverse = params[:reverse].present? ? params[:reverse].to_s.to_boolean(false) : nil
204
203
  @index_options = params.permit(:filter, :filtered_by_key, :routing_scope, :devise_default_routes)
205
204
  .to_h.symbolize_keys.merge(limit: limit, reverse: reverse)
206
205
  end
@@ -16,8 +16,8 @@ if defined?(ActiveJob)
16
16
  # @option options [Boolean] :send_email (true) Whether it sends notification email
17
17
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
18
18
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
19
- # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc) and values are options
20
- # @return [Array<Notificaion>] Array of generated notifications
19
+ # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc.) and values are options
20
+ # @return [Array<Notification>] Array of generated notifications
21
21
  def perform(targets, notifiable, options = {})
22
22
  ActivityNotification::Notification.notify_all(targets, notifiable, options)
23
23
  end
@@ -17,8 +17,8 @@ if defined?(ActiveJob)
17
17
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
18
18
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
19
19
  # @option options [Boolean] :pass_full_options (false) Whether it passes full options to notifiable.notification_targets, not a key only
20
- # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc) and values are options
21
- # @return [Array<Notificaion>] Array of generated notifications
20
+ # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc.) and values are options
21
+ # @return [Array<Notification>] Array of generated notifications
22
22
  def perform(target_type, notifiable, options = {})
23
23
  ActivityNotification::Notification.notify(target_type, notifiable, options)
24
24
  end
@@ -16,7 +16,7 @@ if defined?(ActiveJob)
16
16
  # @option options [Boolean] :send_email (true) Whether it sends notification email
17
17
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
18
18
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
19
- # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc) and values are options
19
+ # @option options [Hash<String, Hash>] :optional_targets ({}) Options for optional targets, keys are optional target name (:amazon_sns or :slack etc.) and values are options
20
20
  # @return [Notification] Generated notification instance
21
21
  def perform(target, notifiable, options = {})
22
22
  ActivityNotification::Notification.notify_to(target, notifiable, options)
@@ -1,5 +1,5 @@
1
1
  if defined?(ActionMailer)
2
- # Mailer for email notification of ActivityNotificaion.
2
+ # Mailer for email notification of ActivityNotification.
3
3
  class ActivityNotification::Mailer < ActivityNotification.config.parent_mailer.constantize
4
4
  include ActivityNotification::Mailers::Helpers
5
5
 
@@ -1,6 +1,6 @@
1
1
  Dear <%= @target.printable_target_name %>
2
2
 
3
- You have recieved follwing notifications.
3
+ You have received the following notifications.
4
4
 
5
5
  <% @notifications.each do |notification| %>
6
6
  <%= notification.notifier.present? ? notification.notifier.printable_notifier_name : 'Someone' %> notified you of <%= notification.notifiable.printable_notifiable_name(notification.target) %><%= " in #{notification.group.printable_group_name}" if notification.group.present? %>.
@@ -67,7 +67,7 @@
67
67
  $(".notifications").prepend(notification.group_owner_view);
68
68
  $(".notification_count").html("<span class=unopened>" + notification.unopened_notification_count + "</span>");
69
69
  }
70
- // Push notificaion using Web Notification API by Push.js
70
+ // Push notification using Web Notification API by Push.js
71
71
  Push.create('ActivityNotification', {
72
72
  body: notification.text,
73
73
  timeout: 5000,
@@ -49,7 +49,7 @@
49
49
  <% target.notifications.filtered_by_key(key).latest.optional_target_names.each do |optional_target_name| %>
50
50
  <div class="field_label">
51
51
  <label>
52
- Optional tagret (<%= optional_target_name %>)
52
+ Optional target (<%= optional_target_name %>)
53
53
  </label>
54
54
  </div>
55
55
  <div class="field">
@@ -70,7 +70,7 @@
70
70
  <% subscription.optional_target_names.each do |optional_target_name| %>
71
71
  <div class="field_label">
72
72
  <label>
73
- Optional tagret (<%= optional_target_name %>)
73
+ Optional target (<%= optional_target_name %>)
74
74
  </label>
75
75
  </div>
76
76
  <div class="field">
data/docs/Functions.md CHANGED
@@ -47,6 +47,19 @@ class Comment < ActiveRecord::Base
47
47
  end
48
48
  ```
49
49
 
50
+ You can also control email delivery per-notification by overriding `notification_email_allowed?` on the notifiable model:
51
+
52
+ ```ruby
53
+ class Comment < ActiveRecord::Base
54
+ # ...acts_as_notifiable configuration...
55
+
56
+ def notification_email_allowed?(target, key)
57
+ # Example: skip email for comments on draft articles
58
+ !article.draft?
59
+ end
60
+ end
61
+ ```
62
+
50
63
  #### Sender configuration
51
64
 
52
65
  You can configure the notification *"from"* address inside of *activity_notification.rb* in two ways.
@@ -202,6 +215,76 @@ class Article < ActiveRecord::Base
202
215
  end
203
216
  ```
204
217
 
218
+ #### Email attachments
219
+
220
+ *activity_notification* supports email attachments at three levels with the same priority order as CC:
221
+
222
+ 1. **Notifiable model override** (highest priority) - using `overriding_notification_email_attachments` method
223
+ 2. **Target model method** - using `mailer_attachments` method
224
+ 3. **Global configuration** - using `config.mailer_attachments` setting
225
+
226
+ Attachments are specified as a Hash (or Array of Hashes) with `:filename` and either `:content` (binary data) or `:path` (local file path). An optional `:mime_type` can be provided; otherwise it is inferred from the filename.
227
+
228
+ ##### Global attachment configuration
229
+
230
+ Configure default attachments in *activity_notification.rb* initializer:
231
+
232
+ ```ruby
233
+ # Single attachment from a local file
234
+ config.mailer_attachments = {
235
+ filename: 'terms.pdf',
236
+ path: Rails.root.join('public', 'terms.pdf')
237
+ }
238
+
239
+ # Multiple attachments
240
+ config.mailer_attachments = [
241
+ { filename: 'logo.png', path: Rails.root.join('app/assets/images/logo.png') },
242
+ { filename: 'terms.pdf', path: Rails.root.join('public', 'terms.pdf') }
243
+ ]
244
+
245
+ # Dynamic attachments based on notification key
246
+ config.mailer_attachments = ->(key) {
247
+ if key.include?('invoice')
248
+ { filename: 'invoice.pdf', content: generate_invoice_pdf }
249
+ else
250
+ nil # No attachments
251
+ end
252
+ }
253
+ ```
254
+
255
+ ##### Target-level attachment configuration
256
+
257
+ Define `mailer_attachments` method in your target model:
258
+
259
+ ```ruby
260
+ class User < ActiveRecord::Base
261
+ acts_as_target
262
+
263
+ def mailer_attachments
264
+ if admin?
265
+ { filename: 'admin_guide.pdf', path: Rails.root.join('docs', 'admin_guide.pdf') }
266
+ else
267
+ nil # Falls back to global config
268
+ end
269
+ end
270
+ end
271
+ ```
272
+
273
+ ##### Notifiable-level attachment override
274
+
275
+ For per-notification attachments, implement `overriding_notification_email_attachments` in your notifiable model:
276
+
277
+ ```ruby
278
+ class Invoice < ActiveRecord::Base
279
+ acts_as_notifiable :users,
280
+ targets: ->(invoice, key) { [invoice.user] }
281
+
282
+ def overriding_notification_email_attachments(target, key)
283
+ { filename: "invoice_#{number}.pdf", content: generate_pdf }
284
+ end
285
+ end
286
+ ```
287
+
205
288
  #### i18n for email
206
289
 
207
290
  The subject of notification email can be put in your locale *.yml* files as **mail_subject** field:
@@ -525,7 +608,7 @@ config.subscribe_to_email_as_default = false
525
608
  config.subscribe_to_optional_targets_as_default = true
526
609
  ```
527
610
 
528
- However if **subscribe_as_default** is not enabled, **subscribe_to_email_as_default** and **subscribe_to_optional_targets_as_default** won't change anything.
611
+ However, if **subscribe_as_default** is not enabled, **subscribe_to_email_as_default** and **subscribe_to_optional_targets_as_default** won't change anything.
529
612
 
530
613
  ##### Creating and updating subscriptions
531
614
 
@@ -860,7 +943,7 @@ See [Devise Token Auth documents](https://devise-token-auth.gitbook.io/devise-to
860
943
 
861
944
  *activity_notification* supports push notification with Action Cable by WebSocket.
862
945
  *activity_notification* only provides Action Cable channels implementation, does not connections.
863
- You can use default implementaion in Rails or your custom `ApplicationCable::Connection` for Action Cable connections.
946
+ You can use default implementation in Rails or your custom `ApplicationCable::Connection` for Action Cable connections.
864
947
 
865
948
  #### Enabling broadcasting notifications to channels
866
949
 
@@ -928,7 +1011,7 @@ You can simply create subscriptions for the specified target in your view like t
928
1011
  received: function(notification) {
929
1012
  // Display notification
930
1013
 
931
- // Push notificaion using Web Notification API by Push.js
1014
+ // Push notification using Web Notification API by Push.js
932
1015
  Push.create('ActivityNotification', {
933
1016
  body: notification.text,
934
1017
  timeout: 5000,
@@ -978,7 +1061,7 @@ export default {
978
1061
  notify (data) {
979
1062
  // Display notification
980
1063
 
981
- // Push notificaion using Web Notification API by Push.js
1064
+ // Push notification using Web Notification API by Push.js
982
1065
  Push.create('ActivityNotification', {
983
1066
  body: data.notification.text,
984
1067
  timeout: 5000,
@@ -1045,7 +1128,7 @@ App.activity_notification = App.cable.subscriptions.create(
1045
1128
 
1046
1129
  *ActivityNotification::NotificationWithDeviseChannel* will confirm subscription requests from authenticated cookies by Devise. If the user has not signed in, the subscription request will be rejected. If the user has signed in as unauthorized user, the subscription request will be also rejected.
1047
1130
 
1048
- In addtion, you can use `Target#notification_action_cable_channel_class_name` method to select channel class depending on your *action_cable_with_devise* configuration for the target.
1131
+ In addition, you can use `Target#notification_action_cable_channel_class_name` method to select channel class depending on your *action_cable_with_devise* configuration for the target.
1049
1132
 
1050
1133
  ```js
1051
1134
  App.activity_notification = App.cable.subscriptions.create(
@@ -1253,7 +1336,7 @@ First, add **slack-notifier** gem to your Gemfile and create Incoming WebHooks i
1253
1336
  gem 'slack-notifier'
1254
1337
  ```
1255
1338
 
1256
- Then, write `require 'activity_notification/optional_targets/slack'` statement in your notifiable model and set *ActivityNotification::OptionalTarget::Slack* to *acts_as_notifiable* with *:webhook_url* and *:target_username* initializing parameters. *:webhook_url* is created WebHook URL and required, *:target_username* is target's slack user name as String value, symbol method name or lambda function and is optional.
1339
+ Then, write `require 'activity_notification/optional_targets/slack'` statement in your notifiable model and set *ActivityNotification::OptionalTarget::Slack* to *acts_as_notifiable* with *:webhook_url* and *:target_username* initializing parameters. *:webhook_url* is created WebHook URL and required, *:target_username* is target's slack username as String value, symbol method name or lambda function and is optional.
1257
1340
  Any other options for `Slack::Notifier.new` are available as initializing parameters. See [Github slack-notifier](https://github.com/stevenosloan/slack-notifier) and [API Reference of Class: Slack::Notifier](http://www.rubydoc.info/gems/slack-notifier/1.5.1/Slack/Notifier) for more details.
1258
1341
 
1259
1342
  ```ruby
@@ -1323,14 +1406,15 @@ end
1323
1406
  *ActivityNotification::Subscription* model provides API to subscribe and unsubscribe optional notification targets. Call these methods with optional target name like this:
1324
1407
 
1325
1408
  ```ruby
1326
- # Subscribe Acltion Cable channel for 'comment.reply' notifications
1409
+ # Subscribe Action Cable channel for 'comment.reply' notifications
1327
1410
  user.find_or_create_subscription('comment.reply').subscribe_to_optional_target(:action_cable_channel)
1328
1411
 
1329
- # Subscribe Acltion Cable API channel for 'comment.reply' notifications
1412
+ # Subscribe Action Cable API channel for 'comment.reply' notifications
1330
1413
  user.find_or_create_subscription('comment.reply').subscribe_to_optional_target(:action_cable_api_channel)
1331
1414
 
1332
1415
  # Unsubscribe Slack notification for 'comment.reply' notifications
1333
1416
  user.find_or_create_subscription('comment.reply').unsubscribe_to_optional_target(:slack)
1334
1417
  ```
1335
1418
 
1336
- You can also manage subscriptions of optional targets by subscriptions REST API. See [REST API backend](#rest-api-backend) for more details.
1419
+ You can also manage subscriptions of optional targets by subscriptions REST API. See [REST API backend](#rest-api-backend) for more details.
1420
+
data/docs/Setup.md CHANGED
@@ -20,7 +20,7 @@ $ bin/rails generate activity_notification:install
20
20
  ```
21
21
 
22
22
  The generator will install an initializer which describes all configuration options of *activity_notification*.
23
- It also generates a i18n based translation file which we can configure the presentation of notifications.
23
+ It also generates an i18n based translation file which we can configure the presentation of notifications.
24
24
 
25
25
  #### ORM Dependencies
26
26
 
@@ -147,7 +147,7 @@ In such cases, you can use **store_with_associated_records** option in initializ
147
147
  config.store_with_associated_records = true
148
148
  ```
149
149
 
150
- When **store_with_associated_records** is set to *false* as default, *activity_notification* stores notificaion records with association like this:
150
+ When **store_with_associated_records** is set to *false* as default, *activity_notification* stores notification records with association like this:
151
151
 
152
152
  ```json
153
153
  {
@@ -181,7 +181,7 @@ When **store_with_associated_records** is set to *false* as default, *activity_n
181
181
  }
182
182
  ```
183
183
 
184
- When you set **store_with_associated_records** to *true*, *activity_notification* stores notificaion records including associated target, notifiable, notifier and several instance methods like this:
184
+ When you set **store_with_associated_records** to *true*, *activity_notification* stores notification records including associated target, notifiable, notifier and several instance methods like this:
185
185
 
186
186
  ```json
187
187
  {
@@ -561,7 +561,7 @@ ActivityNotification::Notification.notify :users, @comment, key: "comment.reply"
561
561
  The first argument is the plural symbol name of your target model, which is configured in notifiable model by *acts_as_notifiable*.
562
562
  The new instances of **ActivityNotification::Notification** model will be generated for the specified targets.
563
563
 
564
- *Hint*: *:key* is a option. Default key `#{notifiable_type}.default` which means *comment.default* will be used without specified key.
564
+ *Hint*: *:key* is an option. Default key `#{notifiable_type}.default` which means *comment.default* will be used without specified key.
565
565
  You can override it by *Notifiable#default_notification_key*.
566
566
 
567
567
  #### Asynchronous notification API with ActiveJob
@@ -580,7 +580,7 @@ You can also use *:notify_later* option in *notify* method. This is the same ope
580
580
 
581
581
  *Note*: *notify_now* is an alias for *notify* and does the same.
582
582
 
583
- When you use asynchronous notification API, you should setup ActiveJob with background queuing service such as Sidekiq.
583
+ When you use asynchronous notification API, you should set up ActiveJob with background queuing service such as Sidekiq.
584
584
  You can set *config.active_job_queue* in your initializer to specify a queue name *activity_notification* will use.
585
585
  The default queue name is *:activity_notification*.
586
586
 
@@ -725,7 +725,7 @@ For example, if you have a notification with *:key* set to *"notification.commen
725
725
 
726
726
  *Hint*: the *"notification."* prefix in *:key* is completely optional, you can skip it in your projects or use this prefix only to make namespace.
727
727
 
728
- If you would like to fallback to a partial, you can utilize the **:fallback** parameter to specify the path of a partial to use when one is missing:
728
+ If you would like to fall back to a partial, you can utilize the **:fallback** parameter to specify the path of a partial to use when one is missing:
729
729
 
730
730
  ```erb
731
731
  <%= render_notification(@notification, target: :users, fallback: :default) %>
@@ -741,7 +741,7 @@ If you do not specify *:target* option like this,
741
741
 
742
742
  the gem will look for a partial in *default* as the target type which means *activity_notification/notifications/default/_default.html.(|erb|haml|slim|something_else)*.
743
743
 
744
- If a view file does not exist then *ActionView::MisingTemplate* will be raised. If you wish to fallback to the old behaviour and use an i18n based translation in this situation you can specify a *:fallback* parameter of *:text* to fallback to this mechanism like such:
744
+ If a view file does not exist then *ActionView::MisingTemplate* will be raised. If you wish to fall back to the old behaviour and use an i18n based translation in this situation you can specify a *:fallback* parameter of *:text* to fall back to this mechanism like such:
745
745
 
746
746
  ```erb
747
747
  <%= render_notification(@notification, fallback: :text) %>
data/docs/Testing.md CHANGED
@@ -149,7 +149,7 @@ $ export AN_ORM=dynamoid AN_TEST_DB=postgresql
149
149
  ```
150
150
 
151
151
  Then, configure *spec/rails_app/config/database.yml* or *spec/rails_app/config/mongoid.yml*, *spec/rails_app/config/dynamoid.rb* as your local database.
152
- Finally, run database migration, seed data script and the example appliation.
152
+ Finally, run database migration, seed data script and the example application.
153
153
  ```console
154
154
  $ cd spec/rails_app
155
155
  $ # You don't need migration when you use MongoDB only (AN_ORM=mongoid and AN_TEST_DB=mongodb)
@@ -0,0 +1,108 @@
1
+ # Upgrade Guide: v2.5.x → v2.6.0
2
+
3
+ ## Overview
4
+
5
+ v2.6.0 adds instance-level subscription support ([#202](https://github.com/simukappu/activity_notification/issues/202)). This requires a database migration for existing installations.
6
+
7
+ **You must run the migration before deploying the updated gem.** The gem will raise errors if the new columns are missing.
8
+
9
+ ## Step 1: Update the gem
10
+
11
+ ```ruby
12
+ # Gemfile
13
+ gem 'activity_notification', '~> 2.6.0'
14
+ ```
15
+
16
+ ```console
17
+ $ bundle update activity_notification
18
+ ```
19
+
20
+ ## Step 2: Run the migration
21
+
22
+ ### ActiveRecord
23
+
24
+ Generate and run the migration:
25
+
26
+ ```console
27
+ $ bin/rails generate activity_notification:add_notifiable_to_subscriptions
28
+ $ bin/rails db:migrate
29
+ ```
30
+
31
+ This will:
32
+ - Add `notifiable_type` (string, nullable) and `notifiable_id` (integer, nullable) columns to the `subscriptions` table
33
+ - Remove the old unique index on `[:target_type, :target_id, :key]`
34
+ - Add a new unique index on `[:target_type, :target_id, :key, :notifiable_type, :notifiable_id]` with prefix lengths for MySQL compatibility
35
+
36
+ ### Mongoid
37
+
38
+ No migration is needed. Mongoid is schemaless and the new fields will be added automatically. However, if you have custom indexes on the subscriptions collection, you may want to update them:
39
+
40
+ ```console
41
+ $ bin/rails db:mongoid:create_indexes
42
+ ```
43
+
44
+ ### Dynamoid
45
+
46
+ No migration is needed. The new `notifiable_key` field will be added automatically to new records.
47
+
48
+ ## Step 3: Verify
49
+
50
+ After migrating, verify that existing subscriptions still work:
51
+
52
+ ```ruby
53
+ # Existing key-level subscriptions should still work
54
+ user.subscribes_to_notification?('comment.default') # => true/false as before
55
+ ```
56
+
57
+ ## What changed
58
+
59
+ ### Subscription queries
60
+
61
+ Key-level subscription lookups now explicitly filter by `notifiable_type IS NULL`. This ensures that instance-level subscriptions (where `notifiable_type` is set) are not confused with key-level subscriptions.
62
+
63
+ Before:
64
+ ```ruby
65
+ subscriptions.where(key: key).first
66
+ ```
67
+
68
+ After:
69
+ ```ruby
70
+ subscriptions.where(key: key, notifiable_type: nil).first
71
+ ```
72
+
73
+ For existing databases where all subscriptions have `NULL` notifiable fields, the results are identical.
74
+
75
+ ### Method signature changes
76
+
77
+ The following methods have new optional keyword arguments. Existing calls without these arguments are fully compatible:
78
+
79
+ - `find_subscription(key, notifiable: nil)` — pass `notifiable:` to look up instance-level subscriptions
80
+ - `find_or_create_subscription(key, subscription_params)` — pass `notifiable:` in `subscription_params` to create instance-level subscriptions
81
+ - `subscribes_to_notification?(key, subscribe_as_default, notifiable: nil)` — pass `notifiable:` to check instance-level subscriptions
82
+
83
+ ### Uniqueness constraint
84
+
85
+ The subscription uniqueness constraint now includes `notifiable_type` and `notifiable_id`. This allows a target to have:
86
+ - One key-level subscription per key (where notifiable is NULL)
87
+ - One instance-level subscription per key per notifiable instance
88
+
89
+ ## Using instance-level subscriptions
90
+
91
+ ```ruby
92
+ # Subscribe a user to notifications from a specific post
93
+ user.create_subscription(
94
+ key: 'comment.default',
95
+ notifiable_type: 'Post',
96
+ notifiable_id: post.id
97
+ )
98
+
99
+ # Check if user subscribes to notifications from this specific post
100
+ user.subscribes_to_notification?('comment.default', notifiable: post)
101
+
102
+ # Find an instance-level subscription
103
+ user.find_subscription('comment.default', notifiable: post)
104
+
105
+ # When notify is called, targets from instance-level subscriptions
106
+ # are automatically merged with notification_targets
107
+ Notification.notify(:users, comment)
108
+ ```