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
@@ -94,6 +94,11 @@ module ActivityNotification
94
94
  headers[header_name] = header_value if header_value
95
95
  end
96
96
  @email = headers[:to]
97
+
98
+ # Resolve attachments
99
+ attachment_specs = resolve_attachments(key)
100
+ headers[:attachment_specs] = attachment_specs if attachment_specs.present?
101
+
97
102
  headers
98
103
  end
99
104
 
@@ -125,6 +130,58 @@ module ActivityNotification
125
130
  end
126
131
  end
127
132
 
133
+ # Returns attachment specification(s) for notification email.
134
+ # Checks target method first, then falls back to global configuration.
135
+ #
136
+ # @param [Object] target Target instance to notify
137
+ # @return [Hash, Array<Hash>, nil] Attachment specification(s) or nil
138
+ def mailer_attachments(target)
139
+ if target.respond_to?(:mailer_attachments)
140
+ target.mailer_attachments
141
+ elsif ActivityNotification.config.mailer_attachments.present?
142
+ if ActivityNotification.config.mailer_attachments.is_a?(Proc)
143
+ key = @notification ? @notification.key : nil
144
+ ActivityNotification.config.mailer_attachments.call(key)
145
+ else
146
+ ActivityNotification.config.mailer_attachments
147
+ end
148
+ else
149
+ nil
150
+ end
151
+ end
152
+
153
+ # Resolves attachment specifications with priority:
154
+ # notifiable override > target method > global configuration.
155
+ #
156
+ # @param [String] key Key of the notification
157
+ # @return [Hash, Array<Hash>, nil] Resolved attachment specification(s) or nil
158
+ def resolve_attachments(key)
159
+ if @notification&.notifiable&.respond_to?(:overriding_notification_email_attachments) &&
160
+ @notification.notifiable.overriding_notification_email_attachments(@target, key).present?
161
+ @notification.notifiable.overriding_notification_email_attachments(@target, key)
162
+ else
163
+ mailer_attachments(@target)
164
+ end
165
+ end
166
+
167
+ # Processes attachment specifications and adds them to the mail object.
168
+ #
169
+ # @param [Mail::Message] mail_obj The mail object to add attachments to
170
+ # @param [Hash, Array<Hash>, nil] specs Attachment specification(s)
171
+ # @return [void]
172
+ def process_attachments(mail_obj, specs)
173
+ return if specs.blank?
174
+ specs_array = specs.is_a?(Array) ? specs : [specs]
175
+ specs_array.each do |spec|
176
+ next if spec.blank?
177
+ validate_attachment_spec!(spec)
178
+ content = spec[:content] || File.read(spec[:path])
179
+ options = { content: content }
180
+ options[:mime_type] = spec[:mime_type] if spec[:mime_type]
181
+ mail_obj.attachments[spec[:filename]] = options
182
+ end
183
+ end
184
+
128
185
  # Returns sender email address as 'reply_to'.
129
186
  #
130
187
  # @param [String] key Key of the notification or batch notification email
@@ -204,17 +261,46 @@ module ActivityNotification
204
261
  # @param [Hash] headers Prepared email header
205
262
  # @param [String, Symbol] fallback Fallback option
206
263
  def send_mail(headers, fallback = nil)
264
+ attachment_specs = headers.delete(:attachment_specs)
207
265
  begin
208
- mail headers
266
+ mail_obj = mail headers
267
+ process_attachments(mail_obj, attachment_specs)
268
+ mail_obj
209
269
  rescue ActionView::MissingTemplate => e
210
270
  if fallback.present?
211
- mail headers.merge(template_name: fallback)
271
+ mail_obj = mail headers.merge(template_name: fallback)
272
+ process_attachments(mail_obj, attachment_specs)
273
+ mail_obj
212
274
  else
213
275
  raise e
214
276
  end
215
277
  end
216
278
  end
217
279
 
280
+ # Validates an attachment specification hash.
281
+ #
282
+ # @param [Hash] spec Attachment specification
283
+ # @raise [ArgumentError] If specification is invalid
284
+ # @return [void]
285
+ def validate_attachment_spec!(spec)
286
+ unless spec.is_a?(Hash)
287
+ raise ArgumentError, "Attachment specification must be a Hash, got #{spec.class}"
288
+ end
289
+ unless spec[:filename].present?
290
+ raise ArgumentError, "Attachment specification must include :filename"
291
+ end
292
+ content_sources = [spec[:content], spec[:path]].compact
293
+ if content_sources.empty?
294
+ raise ArgumentError, "Attachment specification must include :content or :path"
295
+ end
296
+ if content_sources.size > 1
297
+ raise ArgumentError, "Attachment specification must include only one of :content or :path"
298
+ end
299
+ if spec[:path].present? && !File.exist?(spec[:path])
300
+ raise ArgumentError, "Attachment file not found: #{spec[:path]}"
301
+ end
302
+ end
303
+
218
304
  end
219
305
  end
220
306
  end
@@ -14,7 +14,7 @@ module ActivityNotification
14
14
  # Has many notification instances for this notifiable.
15
15
  # Dependency for these notifications can be overridden from acts_as_notifiable.
16
16
  # @scope instance
17
- # @return [Array<Notificaion>, Mongoid::Criteria<Notificaion>] Array or database query of notifications for this notifiable
17
+ # @return [Array<Notification>, Mongoid::Criteria<Notification>] Array or database query of notifications for this notifiable
18
18
  has_many_records :generated_notifications_as_notifiable,
19
19
  class_name: "::ActivityNotification::Notification",
20
20
  as: :notifiable
@@ -65,13 +65,13 @@ module ActivityNotification
65
65
  end
66
66
 
67
67
  # Returns notification targets from configured field or overridden method.
68
- # This method is able to be overridden.
68
+ # This method can be overridden.
69
69
  #
70
70
  # @param [String] target_type Target type to notify
71
71
  # @param [Hash] options Options for notifications
72
72
  # @option options [String] :key (notifiable.default_notification_key) Key of the notification
73
73
  # @option options [Hash] :parameters ({}) Additional parameters of the notifications
74
- # @return [Array<Notificaion> | ActiveRecord_AssociationRelation<Notificaion>] Array or database query of the notification targets
74
+ # @return [Array<Notification> | ActiveRecord_AssociationRelation<Notification>] Array or database query of the notification targets
75
75
  def notification_targets(target_type, options = {})
76
76
  target_typed_method_name = "notification_#{cast_to_resources_name(target_type)}"
77
77
  resolved_parameter = resolve_parameter(
@@ -86,8 +86,40 @@ module ActivityNotification
86
86
  resolved_parameter
87
87
  end
88
88
 
89
+ # Returns targets that have instance-level subscriptions for this notifiable.
90
+ # This method finds all active instance-level subscriptions for this specific notifiable
91
+ # instance and returns their target objects.
92
+ #
93
+ # @param [String] target_type Target type to notify
94
+ # @param [String] key Key of the notification (defaults to default_notification_key)
95
+ # @return [Array<Object>] Array of target instances with active instance-level subscriptions
96
+ def instance_subscription_targets(target_type, key = nil)
97
+ key ||= default_notification_key
98
+ target_class_name = target_type.to_s.to_model_name
99
+ if ActivityNotification.config.orm == :dynamoid
100
+ # :nocov:
101
+ delimiter = ActivityNotification.config.composite_key_delimiter
102
+ Subscription.where(
103
+ notifiable_key: "#{self.class.name}#{delimiter}#{self.id}",
104
+ key: key,
105
+ subscribing: true
106
+ ).select { |s| s.target_type == target_class_name }.map(&:target).compact
107
+ # :nocov:
108
+ else
109
+ # :nocov:
110
+ Subscription.where(
111
+ notifiable_type: self.class.name,
112
+ notifiable_id: self.id,
113
+ key: key,
114
+ subscribing: true,
115
+ target_type: target_class_name
116
+ ).map(&:target).compact
117
+ # :nocov:
118
+ end
119
+ end
120
+
89
121
  # Returns group unit of the notifications from configured field or overridden method.
90
- # This method is able to be overridden.
122
+ # This method can be overridden.
91
123
  #
92
124
  # @param [String] target_type Target type to notify
93
125
  # @param [String] key Key of the notification
@@ -101,7 +133,7 @@ module ActivityNotification
101
133
  end
102
134
 
103
135
  # Returns group expiry period of the notifications from configured field or overridden method.
104
- # This method is able to be overridden.
136
+ # This method can be overridden.
105
137
  #
106
138
  # @param [String] target_type Target type to notify
107
139
  # @param [String] key Key of the notification
@@ -115,7 +147,7 @@ module ActivityNotification
115
147
  end
116
148
 
117
149
  # Returns additional notification parameters from configured field or overridden method.
118
- # This method is able to be overridden.
150
+ # This method can be overridden.
119
151
  #
120
152
  # @param [String] target_type Target type to notify
121
153
  # @param [String] key Key of the notification
@@ -129,7 +161,7 @@ module ActivityNotification
129
161
  end
130
162
 
131
163
  # Returns notifier of the notification from configured field or overridden method.
132
- # This method is able to be overridden.
164
+ # This method can be overridden.
133
165
  #
134
166
  # @param [String] target_type Target type to notify
135
167
  # @param [String] key Key of the notification
@@ -143,7 +175,7 @@ module ActivityNotification
143
175
  end
144
176
 
145
177
  # Returns if sending notification email is allowed for the notifiable from configured field or overridden method.
146
- # This method is able to be overridden.
178
+ # This method can be overridden.
147
179
  #
148
180
  # @param [Object] target Target instance to notify
149
181
  # @param [String] key Key of the notification
@@ -157,7 +189,7 @@ module ActivityNotification
157
189
  end
158
190
 
159
191
  # Returns if publishing WebSocket using ActionCable is allowed for the notifiable from configured field or overridden method.
160
- # This method is able to be overridden.
192
+ # This method can be overridden.
161
193
  #
162
194
  # @param [Object] target Target instance to notify
163
195
  # @param [String] key Key of the notification
@@ -171,7 +203,7 @@ module ActivityNotification
171
203
  end
172
204
 
173
205
  # Returns if publishing WebSocket API using ActionCable is allowed for the notifiable from configured field or overridden method.
174
- # This method is able to be overridden.
206
+ # This method can be overridden.
175
207
  #
176
208
  # @param [Object] target Target instance to notify
177
209
  # @param [String] key Key of the notification
@@ -185,7 +217,7 @@ module ActivityNotification
185
217
  end
186
218
 
187
219
  # Returns notifiable_path to move after opening notification from configured field or overridden method.
188
- # This method is able to be overridden.
220
+ # This method can be overridden.
189
221
  #
190
222
  # @param [String] target_type Target type to notify
191
223
  # @param [String] key Key of the notification
@@ -219,7 +251,7 @@ module ActivityNotification
219
251
  end
220
252
 
221
253
  # Returns optional_targets of the notification from configured field or overridden method.
222
- # This method is able to be overridden.
254
+ # This method can be overridden.
223
255
  #
224
256
  # @param [String] target_type Target type to notify
225
257
  # @param [String] key Key of the notification
@@ -233,7 +265,7 @@ module ActivityNotification
233
265
  end
234
266
 
235
267
  # Returns optional_target names of the notification from configured field or overridden method.
236
- # This method is able to be overridden.
268
+ # This method can be overridden.
237
269
  #
238
270
  # @param [String] target_type Target type to notify
239
271
  # @param [String] key Key of the notification
@@ -275,8 +307,8 @@ module ActivityNotification
275
307
  # @option options [Boolean] :send_email (true) Whether it sends notification email
276
308
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
277
309
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
278
- # @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
279
- # @return [Array<Notificaion>] Array of generated notifications
310
+ # @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
311
+ # @return [Array<Notification>] Array of generated notifications
280
312
  def notify(target_type, options = {})
281
313
  Notification.notify(target_type, self, options)
282
314
  end
@@ -295,8 +327,8 @@ module ActivityNotification
295
327
  # @option options [Boolean] :send_email (true) Whether it sends notification email
296
328
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
297
329
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
298
- # @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
299
- # @return [Array<Notificaion>] Array of generated notifications
330
+ # @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
331
+ # @return [Array<Notification>] Array of generated notifications
300
332
  def notify_later(target_type, options = {})
301
333
  Notification.notify_later(target_type, self, options)
302
334
  end
@@ -316,8 +348,8 @@ module ActivityNotification
316
348
  # @option options [Boolean] :send_email (true) Whether it sends notification email
317
349
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
318
350
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
319
- # @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
320
- # @return [Array<Notificaion>] Array of generated notifications
351
+ # @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
352
+ # @return [Array<Notification>] Array of generated notifications
321
353
  def notify_all(targets, options = {})
322
354
  Notification.notify_all(targets, self, options)
323
355
  end
@@ -337,8 +369,8 @@ module ActivityNotification
337
369
  # @option options [Boolean] :send_email (true) Whether it sends notification email
338
370
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
339
371
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
340
- # @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
341
- # @return [Array<Notificaion>] Array of generated notifications
372
+ # @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
373
+ # @return [Array<Notification>] Array of generated notifications
342
374
  def notify_all_later(targets, options = {})
343
375
  Notification.notify_all_later(targets, self, options)
344
376
  end
@@ -357,7 +389,7 @@ module ActivityNotification
357
389
  # @option options [Boolean] :send_email (true) Whether it sends notification email
358
390
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
359
391
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
360
- # @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
392
+ # @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
361
393
  # @return [Notification] Generated notification instance
362
394
  def notify_to(target, options = {})
363
395
  Notification.notify_to(target, self, options)
@@ -378,14 +410,14 @@ module ActivityNotification
378
410
  # @option options [Boolean] :send_email (true) Whether it sends notification email
379
411
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
380
412
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
381
- # @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
413
+ # @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
382
414
  # @return [Notification] Generated notification instance
383
415
  def notify_later_to(target, options = {})
384
416
  Notification.notify_later_to(target, self, options)
385
417
  end
386
418
 
387
419
  # Returns default key of the notification.
388
- # This method is able to be overridden.
420
+ # This method can be overridden.
389
421
  # "#{to_resource_name}.default" is defined as default key.
390
422
  #
391
423
  # @return [String] Default key of the notification
@@ -394,7 +426,7 @@ module ActivityNotification
394
426
  end
395
427
 
396
428
  # Returns key of the notification for tracked notifiable creation.
397
- # This method is able to be overridden.
429
+ # This method can be overridden.
398
430
  # "#{to_resource_name}.create" is defined as default creation key.
399
431
  #
400
432
  # @return [String] Key of the notification for tracked notifiable creation
@@ -403,7 +435,7 @@ module ActivityNotification
403
435
  end
404
436
 
405
437
  # Returns key of the notification for tracked notifiable update.
406
- # This method is able to be overridden.
438
+ # This method can be overridden.
407
439
  # "#{to_resource_name}.update" is defined as default update key.
408
440
  #
409
441
  # @return [String] Key of the notification for tracked notifiable update
@@ -435,12 +467,10 @@ module ActivityNotification
435
467
  # @api private
436
468
  # @param [String] target_type Target type of generated notifications
437
469
  def generated_notifications_as_notifiable_for(target_type = nil)
438
- target_type.nil? ?
439
- generated_notifications_as_notifiable.all :
440
- generated_notifications_as_notifiable.filtered_by_target_type(target_type.to_s.to_model_name)
470
+ target_type.nil? ? generated_notifications_as_notifiable.all : generated_notifications_as_notifiable.filtered_by_target_type(target_type.to_s.to_model_name)
441
471
  end
442
472
 
443
- # Destroies generated notifications for specified target type with dependency.
473
+ # Destroys generated notifications for specified target type with dependency.
444
474
  # This method is intended to be called before destroy this notifiable as dependent configuration.
445
475
  # @api private
446
476
  # @param [Symbol] dependent Has_many dependency, [:delete_all, :destroy, :restrict_with_error, :restrict_with_exception] are available
@@ -9,7 +9,7 @@ module ActivityNotification
9
9
 
10
10
  # Has many sent notification instances from this notifier.
11
11
  # @scope instance
12
- # @return [Array<Notificaion>, Mongoid::Criteria<Notificaion>] Array or database query of sent notifications from this notifier
12
+ # @return [Array<Notification>, Mongoid::Criteria<Notification>] Array or database query of sent notifications from this notifier
13
13
  has_many_records :sent_notifications,
14
14
  class_name: "::ActivityNotification::Notification",
15
15
  as: :notifier
@@ -27,20 +27,48 @@ module ActivityNotification
27
27
 
28
28
  # Gets subscription of the target and notification key.
29
29
  #
30
- # @param [Hash] key Key of the notification for subscription
30
+ # @param [String] key Key of the notification for subscription
31
+ # @param [Object] notifiable Optional notifiable instance for instance-level subscription lookup
31
32
  # @return [Subscription] Configured subscription instance
32
- def find_subscription(key)
33
- subscriptions.where(key: key).first
33
+ def find_subscription(key, notifiable: nil)
34
+ if notifiable
35
+ if ActivityNotification.config.orm == :dynamoid
36
+ # :nocov:
37
+ delimiter = ActivityNotification.config.composite_key_delimiter
38
+ subscriptions.where(key: key, notifiable_key: "#{notifiable.class.name}#{delimiter}#{notifiable.id}").first
39
+ # :nocov:
40
+ else
41
+ # :nocov:
42
+ subscriptions.where(key: key, notifiable_type: notifiable.class.name, notifiable_id: notifiable.id).first
43
+ # :nocov:
44
+ end
45
+ else
46
+ if ActivityNotification.config.orm == :dynamoid
47
+ # :nocov:
48
+ subscriptions.where(key: key).select { |s| s.notifiable_type.nil? }.first
49
+ # :nocov:
50
+ else
51
+ # :nocov:
52
+ subscriptions.where(key: key, notifiable_type: nil).first
53
+ # :nocov:
54
+ end
55
+ end
34
56
  end
35
57
 
36
58
  # Gets subscription of the target and notification key.
37
59
  #
38
- # @param [Hash] key Key of the notification for subscription
60
+ # @param [String] key Key of the notification for subscription
39
61
  # @param [Hash] subscription_params Parameters to create subscription record
40
62
  # @return [Subscription] Found or created subscription instance
41
63
  def find_or_create_subscription(key, subscription_params = {})
42
- subscription = find_subscription(key)
43
- subscription || create_subscription(subscription_params.merge(key: key))
64
+ notifiable = subscription_params.delete(:notifiable)
65
+ subscription = find_subscription(key, notifiable: notifiable)
66
+ merge_params = { key: key }
67
+ if notifiable
68
+ merge_params[:notifiable_type] = notifiable.class.name
69
+ merge_params[:notifiable_id] = notifiable.id
70
+ end
71
+ subscription || create_subscription(subscription_params.merge(merge_params))
44
72
  end
45
73
 
46
74
  # Creates new subscription of the target.
@@ -65,6 +93,13 @@ module ActivityNotification
65
93
  elsif subscription_params[:subscribing_to_email].nil?
66
94
  subscription_params[:subscribing_to_email] = ActivityNotification.config.subscribe_to_email_as_default
67
95
  end
96
+ # :nocov:
97
+ # Convert notifiable_type/notifiable_id to notifiable_key for Dynamoid
98
+ if ActivityNotification.config.orm == :dynamoid && subscription_params[:notifiable_type].present? && subscription_params[:notifiable_id].present?
99
+ delimiter = ActivityNotification.config.composite_key_delimiter
100
+ subscription_params[:notifiable_key] = "#{subscription_params.delete(:notifiable_type)}#{delimiter}#{subscription_params.delete(:notifiable_id)}"
101
+ end
102
+ # :nocov:
68
103
  subscription = Subscription.new(subscription_params)
69
104
  subscription.assign_attributes(target: self)
70
105
  subscription.subscribing? ?
@@ -101,7 +136,7 @@ module ActivityNotification
101
136
  # @option options [String] :filtered_by_key (nil) Key of the notification for filter
102
137
  # @option options [Array|Hash] :custom_filter (nil) Custom subscription filter (e.g. ["created_at >= ?", time.hour.ago])
103
138
  # @option options [Boolean] :with_target (false) If it includes target with subscriptions
104
- # @return [Array<Notificaion>] Configured subscription index of the target
139
+ # @return [Array<Notification>] Configured subscription index of the target
105
140
  def subscription_index(options = {})
106
141
  target_index = subscriptions.filtered_by_options(options)
107
142
  target_index = options[:reverse] ? target_index.earliest_order : target_index.latest_order
@@ -120,7 +155,7 @@ module ActivityNotification
120
155
  # @option options [Symbol|String] :filter (nil) Filter option to load notification keys (Nothing as all, 'configured' with configured subscriptions or 'unconfigured' without subscriptions)
121
156
  # @option options [String] :filtered_by_key (nil) Key of the notification for filter
122
157
  # @option options [Array|Hash] :custom_filter (nil) Custom subscription filter (e.g. ["created_at >= ?", time.hour.ago])
123
- # @return [Array<Notificaion>] Unconfigured notification keys of the target
158
+ # @return [Array<Notification>] Unconfigured notification keys of the target
124
159
  def notification_keys(options = {})
125
160
  subscription_keys = subscriptions.uniq_keys
126
161
  target_notifications = notifications.filtered_by_options(options.select { |k, _| [:filtered_by_key, :custom_filter].include?(k) })
@@ -146,10 +181,22 @@ module ActivityNotification
146
181
  # @api protected
147
182
  #
148
183
  # @param [String] key Key of the notification
149
- # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record does not configured
184
+ # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record is not configured
150
185
  # @return [Boolean] If the target subscribes to the notification
151
186
  def _subscribes_to_notification?(key, subscribe_as_default = ActivityNotification.config.subscribe_as_default)
152
- evaluate_subscription(subscriptions.where(key: key).first, :subscribing?, subscribe_as_default)
187
+ subscription = _find_key_level_subscription(key)
188
+ evaluate_subscription(subscription, :subscribing?, subscribe_as_default)
189
+ end
190
+
191
+ # Returns if the target subscribes to the notification for a specific notifiable instance.
192
+ # @api protected
193
+ #
194
+ # @param [String] key Key of the notification
195
+ # @param [Object] notifiable Notifiable instance to check subscription for
196
+ # @return [Boolean] If the target has an active instance-level subscription for this notifiable
197
+ def _subscribes_to_notification_for_instance?(key, notifiable)
198
+ instance_sub = find_subscription(key, notifiable: notifiable)
199
+ instance_sub.present? && instance_sub.subscribing?
153
200
  end
154
201
 
155
202
  # Returns if the target subscribes to the notification email.
@@ -157,10 +204,11 @@ module ActivityNotification
157
204
  # @api protected
158
205
  #
159
206
  # @param [String] key Key of the notification
160
- # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record does not configured
207
+ # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record is not configured
161
208
  # @return [Boolean] If the target subscribes to the notification
162
209
  def _subscribes_to_notification_email?(key, subscribe_as_default = ActivityNotification.config.subscribe_to_email_as_default)
163
- evaluate_subscription(subscriptions.where(key: key).first, :subscribing_to_email?, subscribe_as_default)
210
+ subscription = _find_key_level_subscription(key)
211
+ evaluate_subscription(subscription, :subscribing_to_email?, subscribe_as_default)
164
212
  end
165
213
  alias_method :_subscribes_to_email?, :_subscribes_to_notification_email?
166
214
 
@@ -170,20 +218,29 @@ module ActivityNotification
170
218
  #
171
219
  # @param [String] key Key of the notification
172
220
  # @param [String, Symbol] optional_target_name Class name of the optional target implementation (e.g. :amazon_sns, :slack)
173
- # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record does not configured
221
+ # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record is not configured
174
222
  # @return [Boolean] If the target subscribes to the specified optional target
175
223
  def _subscribes_to_optional_target?(key, optional_target_name, subscribe_as_default = ActivityNotification.config.subscribe_to_optional_targets_as_default)
224
+ subscription = _find_key_level_subscription(key)
176
225
  _subscribes_to_notification?(key, subscribe_as_default) &&
177
- evaluate_subscription(subscriptions.where(key: key).first, :subscribing_to_optional_target?, subscribe_as_default, optional_target_name, subscribe_as_default)
226
+ evaluate_subscription(subscription, :subscribing_to_optional_target?, subscribe_as_default, optional_target_name, subscribe_as_default)
178
227
  end
179
228
 
180
229
  private
181
230
 
231
+ # Finds a key-level subscription (where notifiable is nil) for the given key.
232
+ # @api private
233
+ # @param [String] key Key of the notification
234
+ # @return [Subscription, nil] Key-level subscription record or nil
235
+ def _find_key_level_subscription(key)
236
+ find_subscription(key, notifiable: nil)
237
+ end
238
+
182
239
  # Returns if the target subscribes.
183
240
  # @api private
184
241
  # @param [Boolean] record Subscription record
185
242
  # @param [Symbol] field Evaluating subscription field or method of the record
186
- # @param [Boolean] default Default subscription value to use when the subscription record does not configured
243
+ # @param [Boolean] default Default subscription value to use when the subscription record is not configured
187
244
  # @param [Array] args Arguments of evaluating subscription method
188
245
  # @return [Boolean] If the target subscribes
189
246
  def evaluate_subscription(record, field, default, *args)