activity_notification 2.5.0 → 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 +130 -45
  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 +9 -9
@@ -25,7 +25,7 @@ module ActivityNotification
25
25
  # @notifications = @user.notifications.group_owners_only.latest_order
26
26
  # @param [Boolean] reverse If notification index will be ordered as earliest first
27
27
  # @param [Boolean] with_group_members If notification index will include group members
28
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
28
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
29
29
  scope :all_index!, ->(reverse = false, with_group_members = false) {
30
30
  target_index = with_group_members ? self : group_owners_only
31
31
  reverse ? target_index.earliest_order : target_index.latest_order
@@ -36,12 +36,12 @@ module ActivityNotification
36
36
  # is defined same as
37
37
  # ActivityNotification::Notification.unopened_only.group_owners_only.latest_order
38
38
  # @scope class
39
- # @example Get unopened notificaton index of the @user
39
+ # @example Get unopened notification index of the @user
40
40
  # @notifications = @user.notifications.unopened_index
41
41
  # @notifications = @user.notifications.unopened_only.group_owners_only.latest_order
42
42
  # @param [Boolean] reverse If notification index will be ordered as earliest first
43
43
  # @param [Boolean] with_group_members If notification index will include group members
44
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
44
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
45
45
  scope :unopened_index, ->(reverse = false, with_group_members = false) {
46
46
  target_index = with_group_members ? unopened_only : unopened_only.group_owners_only
47
47
  reverse ? target_index.earliest_order : target_index.latest_order
@@ -52,54 +52,54 @@ module ActivityNotification
52
52
  # is defined same as
53
53
  # ActivityNotification::Notification.opened_only(limit).group_owners_only.latest_order
54
54
  # @scope class
55
- # @example Get unopened notificaton index of the @user with limit 10
55
+ # @example Get unopened notification index of the @user with limit 10
56
56
  # @notifications = @user.notifications.opened_index(10)
57
57
  # @notifications = @user.notifications.opened_only(10).group_owners_only.latest_order
58
58
  # @param [Integer] limit Limit to query for opened notifications
59
59
  # @param [Boolean] reverse If notification index will be ordered as earliest first
60
60
  # @param [Boolean] with_group_members If notification index will include group members
61
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
61
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
62
62
  scope :opened_index, ->(limit, reverse = false, with_group_members = false) {
63
63
  target_index = with_group_members ? opened_only(limit) : opened_only(limit).group_owners_only
64
64
  reverse ? target_index.earliest_order : target_index.latest_order
65
65
  }
66
66
 
67
67
  # Selects filtered notifications by target_type.
68
- # @example Get filtered unopened notificatons of User as target type
68
+ # @example Get filtered unopened notification of User as target type
69
69
  # @notifications = ActivityNotification.Notification.unopened_only.filtered_by_target_type('User')
70
70
  # @scope class
71
71
  # @param [String] target_type Target type for filter
72
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
72
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
73
73
  scope :filtered_by_target_type, ->(target_type) { where(target_type: target_type) }
74
74
 
75
75
  # Selects filtered notifications by notifiable_type.
76
- # @example Get filtered unopened notificatons of the @user for Comment notifiable class
76
+ # @example Get filtered unopened notification of the @user for Comment notifiable class
77
77
  # @notifications = @user.notifications.unopened_only.filtered_by_type('Comment')
78
78
  # @scope class
79
79
  # @param [String] notifiable_type Notifiable type for filter
80
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
80
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
81
81
  scope :filtered_by_type, ->(notifiable_type) { where(notifiable_type: notifiable_type) }
82
82
 
83
83
  # Selects filtered notifications by key.
84
- # @example Get filtered unopened notificatons of the @user with key 'comment.reply'
84
+ # @example Get filtered unopened notification of the @user with key 'comment.reply'
85
85
  # @notifications = @user.notifications.unopened_only.filtered_by_key('comment.reply')
86
86
  # @scope class
87
87
  # @param [String] key Key of the notification for filter
88
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
88
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
89
89
  scope :filtered_by_key, ->(key) { where(key: key) }
90
90
 
91
91
  # Selects filtered notifications by notifiable_type, group or key with filter options.
92
- # @example Get filtered unopened notificatons of the @user for Comment notifiable class
92
+ # @example Get filtered unopened notification of the @user for Comment notifiable class
93
93
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_type: 'Comment' })
94
- # @example Get filtered unopened notificatons of the @user for @article as group
94
+ # @example Get filtered unopened notification of the @user for @article as group
95
95
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_group: @article })
96
- # @example Get filtered unopened notificatons of the @user for Article instance id=1 as group
96
+ # @example Get filtered unopened notification of the @user for Article instance id=1 as group
97
97
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_group_type: 'Article', filtered_by_group_id: '1' })
98
- # @example Get filtered unopened notificatons of the @user with key 'comment.reply'
98
+ # @example Get filtered unopened notification of the @user with key 'comment.reply'
99
99
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_key: 'comment.reply' })
100
- # @example Get filtered unopened notificatons of the @user for Comment notifiable class with key 'comment.reply'
100
+ # @example Get filtered unopened notification of the @user for Comment notifiable class with key 'comment.reply'
101
101
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_type: 'Comment', filtered_by_key: 'comment.reply' })
102
- # @example Get custom filtered notificatons of the @user
102
+ # @example Get custom filtered notification of the @user
103
103
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ custom_filter: ["created_at >= ?", time.hour.ago] })
104
104
  # @scope class
105
105
  # @param [Hash] options Options for filter
@@ -111,7 +111,7 @@ module ActivityNotification
111
111
  # @option options [String] :later_than (nil) ISO 8601 format time to filter notification index later than specified time
112
112
  # @option options [String] :earlier_than (nil) ISO 8601 format time to filter notification index earlier than specified time
113
113
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago] with ActiveRecord or {:created_at.gt => time.hour.ago} with Mongoid)
114
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
114
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of filtered notifications
115
115
  scope :filtered_by_options, ->(options = {}) {
116
116
  options = ActivityNotification.cast_to_indifferent_hash(options)
117
117
  filtered_notifications = all
@@ -141,22 +141,22 @@ module ActivityNotification
141
141
  }
142
142
 
143
143
  # Orders by latest (newest) first as created_at: :desc.
144
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of notifications ordered by latest first
144
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of notifications ordered by latest first
145
145
  scope :latest_order, -> { order(created_at: :desc) }
146
146
 
147
147
  # Orders by earliest (older) first as created_at: :asc.
148
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of notifications ordered by earliest first
148
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of notifications ordered by earliest first
149
149
  scope :earliest_order, -> { order(created_at: :asc) }
150
150
 
151
151
  # Orders by latest (newest) first as created_at: :desc.
152
152
  # This method is to be overridden in implementation for each ORM.
153
153
  # @param [Boolean] reverse If notifications will be ordered as earliest first
154
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of ordered notifications
154
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of ordered notifications
155
155
  scope :latest_order!, ->(reverse = false) { reverse ? earliest_order : latest_order }
156
156
 
157
157
  # Orders by earliest (older) first as created_at: :asc.
158
158
  # This method is to be overridden in implementation for each ORM.
159
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of notifications ordered by earliest first
159
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of notifications ordered by earliest first
160
160
  scope :earliest_order!, -> { earliest_order }
161
161
 
162
162
  # Returns latest notification instance.
@@ -224,14 +224,18 @@ module ActivityNotification
224
224
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
225
225
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
226
226
  # @option options [Boolean] :pass_full_options (false) Whether it passes full options to notifiable.notification_targets, not a key only
227
- # @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
228
- # @return [Array<Notificaion>] Array of generated notifications
227
+ # @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
228
+ # @return [Array<Notification>] Array of generated notifications
229
229
  def notify(target_type, notifiable, options = {})
230
230
  if options[:notify_later]
231
231
  notify_later(target_type, notifiable, options)
232
232
  else
233
233
  targets = notifiable.notification_targets(target_type, options[:pass_full_options] ? options : options[:key])
234
- unless targets.blank?
234
+ # Merge targets from instance-level subscriptions and deduplicate
235
+ instance_targets = notifiable.instance_subscription_targets(target_type, options[:key])
236
+ targets = merge_targets(targets, instance_targets)
237
+ # Optimize blank check to avoid loading all records for ActiveRecord relations
238
+ unless targets_empty?(targets)
235
239
  notify_all(targets, notifiable, options)
236
240
  end
237
241
  end
@@ -262,8 +266,8 @@ module ActivityNotification
262
266
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
263
267
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
264
268
  # @option options [Boolean] :pass_full_options (false) Whether it passes full options to notifiable.notification_targets, not a key only
265
- # @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
266
- # @return [Array<Notificaion>] Array of generated notifications
269
+ # @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
270
+ # @return [Array<Notification>] Array of generated notifications
267
271
  def notify_later(target_type, notifiable, options = {})
268
272
  target_type = target_type.to_s if target_type.is_a? Symbol
269
273
  options.delete(:notify_later)
@@ -272,10 +276,17 @@ module ActivityNotification
272
276
 
273
277
  # Generates notifications to specified targets.
274
278
  #
275
- # @example Notify to all users
279
+ # For large target collections, this method uses batch processing to reduce memory consumption:
280
+ # - ActiveRecord::Relation: Uses find_each (loads in batches of 1000 records)
281
+ # - Mongoid::Criteria: Uses each with cursor batching
282
+ # - Arrays: Standard iteration (already in memory)
283
+ #
284
+ # @example Notify to all users (with ActiveRecord relation for memory efficiency)
276
285
  # ActivityNotification::Notification.notify_all User.all, @comment
286
+ # @example Notify to all users with custom batch size
287
+ # ActivityNotification::Notification.notify_all User.all, @comment, batch_size: 500
277
288
  #
278
- # @param [Array<Object>] targets Targets to send notifications
289
+ # @param [ActiveRecord::Relation, Mongoid::Criteria, Array<Object>] targets Targets to send notifications
279
290
  # @param [Object] notifiable Notifiable instance
280
291
  # @param [Hash] options Options for notifications
281
292
  # @option options [String] :key (notifiable.default_notification_key) Key of the notification
@@ -287,23 +298,32 @@ module ActivityNotification
287
298
  # @option options [Boolean] :send_email (true) Whether it sends notification email
288
299
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
289
300
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
290
- # @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
291
- # @return [Array<Notificaion>] Array of generated notifications
301
+ # @option options [Integer] :batch_size (1000) Batch size for ActiveRecord find_each (optional)
302
+ # @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
303
+ # @return [Array<Notification>] Array of generated notifications
292
304
  def notify_all(targets, notifiable, options = {})
293
305
  if options[:notify_later]
294
306
  notify_all_later(targets, notifiable, options)
295
307
  else
296
- targets.map { |target| notify_to(target, notifiable, options) }
308
+ # Optimize for large ActiveRecord relations by using batch processing
309
+ process_targets_in_batches(targets, notifiable, options)
297
310
  end
298
311
  end
299
312
  alias_method :notify_all_now, :notify_all
300
313
 
301
314
  # Generates notifications to specified targets later by ActiveJob queue.
302
315
  #
316
+ # Note: When passing ActiveRecord relations or Mongoid criteria to async jobs,
317
+ # they may be serialized to arrays before job execution, which can consume memory
318
+ # for large target sets. For very large datasets (10,000+ records), consider using
319
+ # notify_later with target_type instead, which generates notifications asynchronously
320
+ # without loading all targets upfront:
321
+ # ActivityNotification::Notification.notify(:users, @comment, notify_later: true)
322
+ #
303
323
  # @example Notify to all users later
304
324
  # ActivityNotification::Notification.notify_all_later User.all, @comment
305
325
  #
306
- # @param [Array<Object>] targets Targets to send notifications
326
+ # @param [ActiveRecord::Relation, Mongoid::Criteria, Array<Object>] targets Targets to send notifications
307
327
  # @param [Object] notifiable Notifiable instance
308
328
  # @param [Hash] options Options for notifications
309
329
  # @option options [String] :key (notifiable.default_notification_key) Key of the notification
@@ -314,8 +334,8 @@ module ActivityNotification
314
334
  # @option options [Boolean] :send_email (true) Whether it sends notification email
315
335
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
316
336
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
317
- # @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
318
- # @return [Array<Notificaion>] Array of generated notifications
337
+ # @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
338
+ # @return [Array<Notification>] Array of generated notifications
319
339
  def notify_all_later(targets, notifiable, options = {})
320
340
  options.delete(:notify_later)
321
341
  ActivityNotification::NotifyAllJob.perform_later(targets, notifiable, options)
@@ -324,7 +344,7 @@ module ActivityNotification
324
344
  # Generates notifications to one target.
325
345
  #
326
346
  # @example Notify to one user
327
- # ActivityNotification::Notification.notify_to @comment.auther, @comment
347
+ # ActivityNotification::Notification.notify_to @comment.author, @comment
328
348
  #
329
349
  # @param [Object] target Target to send notifications
330
350
  # @param [Object] notifiable Notifiable instance
@@ -338,7 +358,7 @@ module ActivityNotification
338
358
  # @option options [Boolean] :send_email (true) Whether it sends notification email
339
359
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
340
360
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
341
- # @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
+ # @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
342
362
  # @return [Notification] Generated notification instance
343
363
  def notify_to(target, notifiable, options = {})
344
364
  if options[:notify_later]
@@ -366,7 +386,7 @@ module ActivityNotification
366
386
  # Generates notifications to one target later by ActiveJob queue.
367
387
  #
368
388
  # @example Notify to one user later
369
- # ActivityNotification::Notification.notify_later_to @comment.auther, @comment
389
+ # ActivityNotification::Notification.notify_later_to @comment.author, @comment
370
390
  #
371
391
  # @param [Object] target Target to send notifications
372
392
  # @param [Object] notifiable Notifiable instance
@@ -379,7 +399,7 @@ module ActivityNotification
379
399
  # @option options [Boolean] :send_email (true) Whether it sends notification email
380
400
  # @option options [Boolean] :send_later (true) Whether it sends notification email asynchronously
381
401
  # @option options [Boolean] :publish_optional_targets (true) Whether it publishes notification to optional targets
382
- # @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
402
+ # @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
383
403
  # @return [Notification] Generated notification instance
384
404
  def notify_later_to(target, notifiable, options = {})
385
405
  options.delete(:notify_later)
@@ -396,7 +416,7 @@ module ActivityNotification
396
416
  # @option options [Hash] :parameters ({}) Additional parameters of the notifications
397
417
  def generate_notification(target, notifiable, options = {})
398
418
  key = options[:key] || notifiable.default_notification_key
399
- if target.subscribes_to_notification?(key)
419
+ if target.subscribes_to_notification?(key, notifiable: notifiable)
400
420
  # Store notification
401
421
  notification = store_notification(target, notifiable, key, options)
402
422
  end
@@ -474,7 +494,7 @@ module ActivityNotification
474
494
  # Returns if group member of the notifications exists.
475
495
  # This method is designed to be called from controllers or views to avoid N+1.
476
496
  #
477
- # @param [Array<Notificaion>, ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] notifications Array or database query of the notifications to test member exists
497
+ # @param [Array<Notification>, ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] notifications Array or database query of the notifications to test member exists
478
498
  # @return [Boolean] If group member of the notifications exists
479
499
  def group_member_exists?(notifications)
480
500
  notifications.present? and group_members_of_owner_ids_only(notifications.map(&:id)).exists?
@@ -503,7 +523,7 @@ module ActivityNotification
503
523
 
504
524
  # Returns available options for kinds of notify methods.
505
525
  #
506
- # @return [Array<Notificaion>] Available options for kinds of notify methods
526
+ # @return [Array<Notification>] Available options for kinds of notify methods
507
527
  def available_options
508
528
  [:key, :group, :group_expiry_delay, :notifier, :parameters, :send_email, :send_later, :pass_full_options].freeze
509
529
  end
@@ -520,7 +540,7 @@ module ActivityNotification
520
540
  # @param [String] key Key of the notification
521
541
  # @param [Object] group Group unit of the notifications
522
542
  # @param [ActiveSupport::Duration] group_expiry_delay Expiry period of a notification group
523
- # @return [Notificaion] Valid group owner within the expiration period
543
+ # @return [Notification] Valid group owner within the expiration period
524
544
  def valid_group_owner(target, notifiable, key, group, group_expiry_delay)
525
545
  return nil if group.blank?
526
546
  # Bundle notification group by target, notifiable_type, group and key
@@ -549,6 +569,71 @@ module ActivityNotification
549
569
  notification.after_store
550
570
  notification
551
571
  end
572
+
573
+ # Checks if targets collection is empty without loading all records
574
+ # @api private
575
+ # @param [Object] targets Targets collection (can be an ActiveRecord::Relation, Mongoid::Criteria, Array, etc.)
576
+ # @return [Boolean] True if targets is empty
577
+ def targets_empty?(targets)
578
+ # For ActiveRecord relations and Mongoid criteria, use exists? to avoid loading all records
579
+ if targets.respond_to?(:exists?)
580
+ !targets.exists?
581
+ else
582
+ # For arrays and other enumerables, use blank?
583
+ targets.blank?
584
+ end
585
+ end
586
+
587
+ # Merges instance subscription targets with the main targets list, deduplicating.
588
+ # @api private
589
+ #
590
+ # @param [Object] targets Main targets collection (can be an ActiveRecord::Relation, Mongoid::Criteria, Array, etc.)
591
+ # @param [Array] instance_targets Targets from instance-level subscriptions
592
+ # @return [Array] Deduplicated array of all targets
593
+ def merge_targets(targets, instance_targets)
594
+ return targets if instance_targets.blank?
595
+ all_targets = targets.respond_to?(:to_a) ? targets.to_a : Array(targets)
596
+ (all_targets + instance_targets).uniq
597
+ end
598
+
599
+ # Processes targets in batches for memory efficiency with large collections
600
+ # @api private
601
+ #
602
+ # For ActiveRecord::Relation, uses find_each which loads records in batches (default 1000).
603
+ # For Mongoid::Criteria, uses each which leverages MongoDB's cursor batching.
604
+ # For Arrays and other enumerables, uses standard iteration.
605
+ #
606
+ # Note: When called from async jobs (notify_all_later), ActiveRecord relations may be
607
+ # serialized to arrays before reaching this method, which limits batch processing benefits.
608
+ # Consider using notify_later with target_type instead of notify_all_later with relations
609
+ # for large datasets in async scenarios.
610
+ #
611
+ # @param [Object] targets Targets collection (can be an ActiveRecord::Relation, Mongoid::Criteria, Array, etc.)
612
+ # @param [Object] notifiable Notifiable instance
613
+ # @param [Hash] options Options for notifications
614
+ # @option options [Integer] :batch_size (1000) Batch size for ActiveRecord find_each (optional)
615
+ # @return [Array<Notification>] Array of generated notifications
616
+ def process_targets_in_batches(targets, notifiable, options = {})
617
+ notifications = []
618
+
619
+ # For ActiveRecord relations, use find_each to process in batches
620
+ # This loads records in batches (default 1000) to avoid loading all records into memory
621
+ if targets.respond_to?(:find_each)
622
+ batch_options = {}
623
+ batch_options[:batch_size] = options[:batch_size] if options[:batch_size]
624
+
625
+ targets.find_each(**batch_options) do |target|
626
+ notification = notify_to(target, notifiable, options)
627
+ notifications << notification
628
+ end
629
+ else
630
+ # For arrays and other enumerables, use standard map approach
631
+ # Already in memory, so no batching benefit
632
+ notifications = targets.map { |target| notify_to(target, notifiable, options) }
633
+ end
634
+
635
+ notifications
636
+ end
552
637
  end
553
638
 
554
639
  # :nocov:
@@ -713,7 +798,7 @@ module ActivityNotification
713
798
  # Returns the latest group member notification instance of this notification.
714
799
  # If this group owner has no group members, group owner instance self will be returned.
715
800
  #
716
- # @return [Notificaion] Notification instance of the latest group member notification
801
+ # @return [Notification] Notification instance of the latest group member notification
717
802
  def latest_group_member
718
803
  notification = group_member? && group_owner.present? ? group_owner : self
719
804
  notification.group_member_exists? ? notification.group_members.latest : self
@@ -721,7 +806,7 @@ module ActivityNotification
721
806
 
722
807
  # Remove from notification group and make a new group owner.
723
808
  #
724
- # @return [Notificaion] New group owner instance of the notification group
809
+ # @return [Notification] New group owner instance of the notification group
725
810
  def remove_from_group
726
811
  new_group_owner = group_members.earliest
727
812
  if new_group_owner.present?
@@ -11,7 +11,7 @@ module ActivityNotification
11
11
  # @subscriptions = @user.subscriptions.filtered_by_key('comment.reply')
12
12
  # @scope class
13
13
  # @param [String] key Key of the subscription for filter
14
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of filtered subscriptions
14
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of filtered subscriptions
15
15
  scope :filtered_by_key, ->(key) { where(key: key) }
16
16
 
17
17
  # Selects filtered subscriptions by key with filter options.
@@ -23,7 +23,7 @@ module ActivityNotification
23
23
  # @param [Hash] options Options for filter
24
24
  # @option options [String] :filtered_by_key (nil) Key of the subscription for filter
25
25
  # @option options [Array|Hash] :custom_filter (nil) Custom subscription filter (e.g. ["created_at >= ?", time.hour.ago] or ['created_at.gt': time.hour.ago])
26
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of filtered subscriptions
26
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of filtered subscriptions
27
27
  scope :filtered_by_options, ->(options = {}) {
28
28
  options = ActivityNotification.cast_to_indifferent_hash(options)
29
29
  filtered_subscriptions = all
@@ -37,34 +37,34 @@ module ActivityNotification
37
37
  }
38
38
 
39
39
  # Orders by latest (newest) first as created_at: :desc.
40
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by latest first
40
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by latest first
41
41
  scope :latest_order, -> { order(created_at: :desc) }
42
42
 
43
43
  # Orders by earliest (older) first as created_at: :asc.
44
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by earliest first
44
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by earliest first
45
45
  scope :earliest_order, -> { order(created_at: :asc) }
46
46
 
47
47
  # Orders by latest (newest) first as created_at: :desc.
48
48
  # This method is to be overridden in implementation for each ORM.
49
49
  # @param [Boolean] reverse If subscriptions will be ordered as earliest first
50
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of ordered subscriptions
50
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of ordered subscriptions
51
51
  scope :latest_order!, ->(reverse = false) { reverse ? earliest_order : latest_order }
52
52
 
53
53
  # Orders by earliest (older) first as created_at: :asc.
54
54
  # This method is to be overridden in implementation for each ORM.
55
- # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by earliest first
55
+ # @return [ActiveRecord_AssociationRelation<Notification>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by earliest first
56
56
  scope :earliest_order!, -> { earliest_order }
57
57
 
58
58
  # Orders by latest (newest) first as subscribed_at: :desc.
59
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by latest subscribed_at first
59
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by latest subscribed_at first
60
60
  scope :latest_subscribed_order, -> { order(subscribed_at: :desc) }
61
61
 
62
62
  # Orders by earliest (older) first as subscribed_at: :asc.
63
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by earliest subscribed_at first
63
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by earliest subscribed_at first
64
64
  scope :earliest_subscribed_order, -> { order(subscribed_at: :asc) }
65
65
 
66
66
  # Orders by key name as key: :asc.
67
- # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notificaion>] Database query of subscriptions ordered by key name
67
+ # @return [ActiveRecord_AssociationRelation<Subscription>, Mongoid::Criteria<Notification>] Database query of subscriptions ordered by key name
68
68
  scope :key_order, -> { order(key: :asc) }
69
69
 
70
70
  # Convert Time value to store in database as Hash value.
@@ -182,7 +182,7 @@ module ActivityNotification
182
182
  # Returns if the target subscribes to the specified optional target.
183
183
  #
184
184
  # @param [Symbol] optional_target_name Symbol class name of the optional target implementation (e.g. :amazon_sns, :slack)
185
- # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record does not configured
185
+ # @param [Boolean] subscribe_as_default Default subscription value to use when the subscription record is not configured
186
186
  # @return [Boolean] If the target subscribes to the specified optional target
187
187
  def subscribing_to_optional_target?(optional_target_name, subscribe_as_default = ActivityNotification.config.subscribe_to_optional_targets_as_default)
188
188
  optional_target_key = Subscription.to_optional_target_key(optional_target_name)
@@ -105,7 +105,7 @@ module ActivityNotification
105
105
  end
106
106
  end
107
107
 
108
- # Convets to class name.
108
+ # Converts to class name.
109
109
  # This function returns base_class name for STI models if the class responds to base_class method.
110
110
  # @see https://github.com/simukappu/activity_notification/issues/89
111
111
  # @see https://github.com/simukappu/activity_notification/pull/139
@@ -114,26 +114,26 @@ module ActivityNotification
114
114
  self.class.respond_to?(:base_class) ? self.class.base_class.name : self.class.name
115
115
  end
116
116
 
117
- # Convets to singularized model name (resource name).
117
+ # Converts to singularized model name (resource name).
118
118
  # @return [String] Singularized model name (resource name)
119
119
  def to_resource_name
120
120
  self.to_class_name.demodulize.singularize.underscore
121
121
  end
122
122
 
123
- # Convets to pluralized model name (resources name).
123
+ # Converts to pluralized model name (resources name).
124
124
  # @return [String] Pluralized model name (resources name)
125
125
  def to_resources_name
126
126
  self.to_class_name.demodulize.pluralize.underscore
127
127
  end
128
128
 
129
- # Convets to printable model type name to be humanized.
129
+ # Converts to printable model type name to be humanized.
130
130
  # @return [String] Printable model type name
131
131
  # @todo Is this the best to make readable?
132
132
  def printable_type
133
133
  "#{self.to_class_name.demodulize.humanize}"
134
134
  end
135
135
 
136
- # Convets to printable model name to show in view or email.
136
+ # Converts to printable model name to show in view or email.
137
137
  # @return [String] Printable model name
138
138
  def printable_name
139
139
  "#{self.printable_type} (#{id})"
@@ -91,6 +91,15 @@ module ActivityNotification
91
91
  # @return [String, Array<String>, Proc] CC email address(es) for notification email.
92
92
  attr_accessor :mailer_cc
93
93
 
94
+ # @overload mailer_attachments
95
+ # Returns attachment specification(s) for notification emails
96
+ # @return [Hash, Array<Hash>, Proc, nil] Attachment specification(s) for notification emails.
97
+ # @overload mailer_attachments=(value)
98
+ # Sets attachment specification(s) for notification emails
99
+ # @param [Hash, Array<Hash>, Proc, nil] mailer_attachments The new mailer_attachments
100
+ # @return [Hash, Array<Hash>, Proc, nil] Attachment specification(s) for notification emails.
101
+ attr_accessor :mailer_attachments
102
+
94
103
  # @overload mailer
95
104
  # Returns mailer class for email notification
96
105
  # @return [String] Mailer class for email notification.
@@ -173,8 +182,8 @@ module ActivityNotification
173
182
  attr_accessor :composite_key_delimiter
174
183
 
175
184
  # @overload store_with_associated_records
176
- # Returns whether activity_notification stores notificaion records including associated records like target and notifiable
177
- # @return [Boolean] Whether activity_notification stores notificaion records including associated records like target and notifiable.
185
+ # Returns whether activity_notification stores notification records including associated records like target and notifiable
186
+ # @return [Boolean] Whether activity_notification stores Notification records including associated records like target and notifiable.
178
187
  attr_reader :store_with_associated_records
179
188
 
180
189
  # @overload action_cable_enabled
@@ -246,6 +255,7 @@ module ActivityNotification
246
255
  @subscribe_to_optional_targets_as_default = nil
247
256
  @mailer_sender = nil
248
257
  @mailer_cc = nil
258
+ @mailer_attachments = nil
249
259
  @mailer = 'ActivityNotification::Mailer'
250
260
  @parent_mailer = 'ActionMailer::Base'
251
261
  @parent_job = 'ActiveJob::Base'
@@ -271,10 +281,10 @@ module ActivityNotification
271
281
  @orm = orm.to_sym
272
282
  end
273
283
 
274
- # Sets whether activity_notification stores notificaion records including associated records like target and notifiable.
284
+ # Sets whether activity_notification stores notification records including associated records like target and notifiable.
275
285
  # This store_with_associated_records option can be set true only when you use mongoid or dynamoid ORM.
276
286
  # @param [Boolean] store_with_associated_records The new store_with_associated_records
277
- # @return [Boolean] Whether activity_notification stores notificaion records including associated records like target and notifiable.
287
+ # @return [Boolean] Whether activity_notification stores notification records including associated records like target and notifiable.
278
288
  def store_with_associated_records=(store_with_associated_records)
279
289
  if store_with_associated_records && [:mongoid, :dynamoid].exclude?(@orm) then raise ActivityNotification::ConfigError, "config.store_with_associated_records can be set true only when you use mongoid or dynamoid ORM." end
280
290
  @store_with_associated_records = store_with_associated_records
@@ -289,7 +299,7 @@ module ActivityNotification
289
299
  end
290
300
 
291
301
  # Returns default optional target subscription value to use when the subscription record does not configured
292
- # @return [Boolean] Default optinal target subscription value to use when the subscription record does not configured.
302
+ # @return [Boolean] Default optional target subscription value to use when the subscription record does not configured.
293
303
  def subscribe_to_optional_targets_as_default
294
304
  return false unless @subscribe_as_default
295
305
 
@@ -67,14 +67,12 @@ module ActivityNotification
67
67
  end
68
68
 
69
69
  # Returns path of the target view templates.
70
- # Do not make this method public unless Rendarable module calls controller's target_view_path method to render resources.
70
+ # Do not make this method public unless Renderable module calls controller's target_view_path method to render resources.
71
71
  # @api protected
72
72
  def target_view_path
73
73
  target_type = @target.to_resources_name
74
74
  view_path = [controller_path, target_type].join('/')
75
- lookup_context.exists?(action_name, view_path) ?
76
- view_path :
77
- [controller_path, DEFAULT_VIEW_DIRECTORY].join('/')
75
+ lookup_context.exists?(action_name, view_path) ? view_path : [controller_path, DEFAULT_VIEW_DIRECTORY].join('/')
78
76
  end
79
77
 
80
78
  # Sets view prefixes for target view path.
@@ -14,7 +14,7 @@ module ActivityNotification
14
14
  # Authenticate devise resource by Devise (e.g. calling authenticate_user! method).
15
15
  # @api protected
16
16
  # @todo Needs to call authenticate method by more secure way
17
- # @return [Response] Redirects for unsigned in target by Devise, returns HTTP 403 without neccesary target method or returns 400 when request parameters are not enough
17
+ # @return [Response] Redirects for unsigned in target by Devise, returns HTTP 403 without necessary target method or returns 400 when request parameters are not enough
18
18
  def authenticate_devise_resource!
19
19
  if params[:devise_type].present?
20
20
  authenticate_method_name = "authenticate_#{params[:devise_type].to_resource_name}!"
@@ -29,7 +29,7 @@ module ActivityNotification
29
29
  end
30
30
 
31
31
  # Sets @target instance variable from request parameters.
32
- # This method override super (ActivityNotiication::CommonController#set_target)
32
+ # This method override super (ActivityNotification::CommonController#set_target)
33
33
  # to set devise authenticated target when the target_id params is not specified.
34
34
  # @api protected
35
35
  # @return [Object] Target instance (Returns HTTP 400 when request parameters are not enough)