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.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/app/channels/activity_notification/notification_api_channel.rb +5 -5
- data/app/channels/activity_notification/notification_api_with_devise_channel.rb +4 -4
- data/app/channels/activity_notification/notification_channel.rb +4 -0
- data/app/channels/activity_notification/notification_with_devise_channel.rb +4 -4
- data/app/controllers/activity_notification/notifications_controller.rb +1 -2
- data/app/controllers/activity_notification/subscriptions_controller.rb +2 -3
- data/app/jobs/activity_notification/notify_all_job.rb +2 -2
- data/app/jobs/activity_notification/notify_job.rb +2 -2
- data/app/jobs/activity_notification/notify_to_job.rb +1 -1
- data/app/mailers/activity_notification/mailer.rb +1 -1
- data/app/views/activity_notification/mailer/default/batch_default.text.erb +1 -1
- data/app/views/activity_notification/notifications/default/index.html.erb +1 -1
- data/app/views/activity_notification/subscriptions/default/_notification_keys.html.erb +1 -1
- data/app/views/activity_notification/subscriptions/default/_subscription.html.erb +1 -1
- data/docs/Functions.md +93 -9
- data/docs/Setup.md +7 -7
- data/docs/Testing.md +1 -1
- data/docs/Upgrade-to-2.6.md +108 -0
- data/lib/activity_notification/apis/notification_api.rb +130 -45
- data/lib/activity_notification/apis/subscription_api.rb +10 -10
- data/lib/activity_notification/common.rb +5 -5
- data/lib/activity_notification/config.rb +15 -5
- data/lib/activity_notification/controllers/common_controller.rb +2 -4
- data/lib/activity_notification/controllers/devise_authentication_controller.rb +2 -2
- data/lib/activity_notification/helpers/polymorphic_helpers.rb +6 -6
- data/lib/activity_notification/helpers/view_helpers.rb +3 -3
- data/lib/activity_notification/mailers/helpers.rb +88 -2
- data/lib/activity_notification/models/concerns/notifiable.rb +60 -30
- data/lib/activity_notification/models/concerns/notifier.rb +1 -1
- data/lib/activity_notification/models/concerns/subscriber.rb +72 -15
- data/lib/activity_notification/models/concerns/target.rb +38 -35
- data/lib/activity_notification/optional_targets/action_cable_api_channel.rb +1 -1
- data/lib/activity_notification/optional_targets/slack.rb +2 -2
- data/lib/activity_notification/orm/active_record/notification.rb +25 -25
- data/lib/activity_notification/orm/active_record/subscription.rb +21 -1
- data/lib/activity_notification/orm/dynamoid/extension.rb +3 -3
- data/lib/activity_notification/orm/dynamoid/subscription.rb +8 -1
- data/lib/activity_notification/orm/dynamoid.rb +18 -18
- data/lib/activity_notification/orm/mongoid/notification.rb +26 -28
- data/lib/activity_notification/orm/mongoid/subscription.rb +21 -1
- data/lib/activity_notification/orm/mongoid.rb +1 -1
- data/lib/activity_notification/rails/routes.rb +11 -11
- data/lib/activity_notification/roles/acts_as_group.rb +1 -1
- data/lib/activity_notification/roles/acts_as_notifiable.rb +5 -5
- data/lib/activity_notification/roles/acts_as_notifier.rb +1 -1
- data/lib/activity_notification/roles/acts_as_target.rb +1 -1
- data/lib/activity_notification/version.rb +1 -1
- data/lib/generators/activity_notification/add_notifiable_to_subscriptions/add_notifiable_to_subscriptions_generator.rb +23 -0
- data/lib/generators/activity_notification/add_notifiable_to_subscriptions/templates/add_notifiable_to_subscriptions.rb +13 -0
- data/lib/generators/templates/activity_notification.rb +14 -2
- data/lib/generators/templates/migrations/migration.rb +4 -2
- 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<
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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
|
-
#
|
|
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 [
|
|
291
|
-
# @
|
|
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
|
-
|
|
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<
|
|
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.
|
|
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.
|
|
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<
|
|
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<
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
177
|
-
# @return [Boolean] Whether activity_notification stores
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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)
|