activity_notification 1.7.1 → 2.0.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 (97) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.travis.yml +16 -2
  4. data/CHANGELOG.md +22 -2
  5. data/Gemfile +7 -0
  6. data/Procfile +2 -0
  7. data/README.md +366 -32
  8. data/Rakefile +19 -10
  9. data/activity_notification.gemspec +5 -3
  10. data/app/channels/activity_notification/notification_channel.rb +37 -0
  11. data/app/channels/activity_notification/notification_with_devise_channel.rb +51 -0
  12. data/app/controllers/activity_notification/notifications_controller.rb +1 -1
  13. data/app/controllers/activity_notification/subscriptions_controller.rb +1 -1
  14. data/app/jobs/activity_notification/notify_all_job.rb +16 -0
  15. data/app/jobs/activity_notification/notify_job.rb +17 -0
  16. data/app/jobs/activity_notification/notify_to_job.rb +16 -0
  17. data/app/views/activity_notification/notifications/default/_default_without_grouping.html.erb +1 -1
  18. data/app/views/activity_notification/notifications/default/index.html.erb +55 -2
  19. data/bin/_dynamodblocal +4 -0
  20. data/{scripts → bin}/bundle_update.sh +1 -0
  21. data/bin/deploy_on_heroku.sh +14 -0
  22. data/bin/install_dynamodblocal.sh +5 -0
  23. data/bin/start_dynamodblocal.sh +47 -0
  24. data/bin/stop_dynamodblocal.sh +34 -0
  25. data/gemfiles/Gemfile.rails-4.2 +1 -0
  26. data/gemfiles/Gemfile.rails-5.0 +2 -0
  27. data/gemfiles/Gemfile.rails-5.1 +1 -0
  28. data/gemfiles/Gemfile.rails-5.2 +1 -0
  29. data/gemfiles/Gemfile.rails-6.0.rc +21 -0
  30. data/lib/activity_notification.rb +1 -0
  31. data/lib/activity_notification/apis/notification_api.rb +289 -136
  32. data/lib/activity_notification/apis/subscription_api.rb +80 -53
  33. data/lib/activity_notification/common.rb +3 -3
  34. data/lib/activity_notification/config.rb +89 -33
  35. data/lib/activity_notification/controllers/common_controller.rb +19 -7
  36. data/lib/activity_notification/helpers/errors.rb +4 -0
  37. data/lib/activity_notification/helpers/view_helpers.rb +1 -1
  38. data/lib/activity_notification/models/concerns/notifiable.rb +61 -53
  39. data/lib/activity_notification/models/concerns/subscriber.rb +7 -6
  40. data/lib/activity_notification/models/concerns/target.rb +73 -28
  41. data/lib/activity_notification/optional_targets/base.rb +2 -2
  42. data/lib/activity_notification/orm/active_record/notification.rb +4 -23
  43. data/lib/activity_notification/orm/dynamoid.rb +495 -0
  44. data/lib/activity_notification/orm/dynamoid/extension.rb +184 -0
  45. data/lib/activity_notification/orm/dynamoid/notification.rb +189 -0
  46. data/lib/activity_notification/orm/dynamoid/subscription.rb +82 -0
  47. data/lib/activity_notification/orm/mongoid.rb +4 -1
  48. data/lib/activity_notification/orm/mongoid/notification.rb +8 -25
  49. data/lib/activity_notification/orm/mongoid/subscription.rb +1 -1
  50. data/lib/activity_notification/roles/acts_as_notifiable.rb +33 -5
  51. data/lib/activity_notification/roles/acts_as_target.rb +62 -9
  52. data/lib/activity_notification/version.rb +1 -1
  53. data/lib/generators/templates/activity_notification.rb +30 -7
  54. data/lib/tasks/activity_notification_tasks.rake +14 -4
  55. data/spec/channels/notification_channel_shared_examples.rb +59 -0
  56. data/spec/channels/notification_channel_spec.rb +50 -0
  57. data/spec/channels/notification_with_devise_channel_spec.rb +99 -0
  58. data/spec/concerns/apis/notification_api_spec.rb +2 -2
  59. data/spec/concerns/apis/subscription_api_spec.rb +2 -2
  60. data/spec/concerns/models/notifiable_spec.rb +72 -7
  61. data/spec/concerns/models/subscriber_spec.rb +53 -49
  62. data/spec/concerns/models/target_spec.rb +135 -13
  63. data/spec/config_spec.rb +41 -1
  64. data/spec/controllers/notifications_controller_shared_examples.rb +7 -3
  65. data/spec/controllers/subscriptions_controller_shared_examples.rb +7 -3
  66. data/spec/helpers/view_helpers_spec.rb +12 -10
  67. data/spec/models/dummy/dummy_group_spec.rb +4 -0
  68. data/spec/models/dummy/dummy_notifiable_spec.rb +4 -0
  69. data/spec/models/dummy/dummy_notifier_spec.rb +4 -0
  70. data/spec/models/dummy/dummy_subscriber_spec.rb +3 -0
  71. data/spec/models/dummy/dummy_target_spec.rb +4 -0
  72. data/spec/models/notification_spec.rb +164 -45
  73. data/spec/models/subscription_spec.rb +69 -14
  74. data/spec/orm/dynamoid_spec.rb +115 -0
  75. data/spec/rails_app/app/assets/javascripts/application.js +2 -1
  76. data/spec/rails_app/app/assets/javascripts/cable.js +12 -0
  77. data/spec/rails_app/app/controllers/comments_controller.rb +3 -4
  78. data/spec/rails_app/app/models/admin.rb +6 -4
  79. data/spec/rails_app/app/models/article.rb +2 -2
  80. data/spec/rails_app/app/models/comment.rb +17 -5
  81. data/spec/rails_app/app/models/user.rb +5 -3
  82. data/spec/rails_app/app/views/activity_notification/notifications/users/overridden/custom/_test.html.erb +1 -0
  83. data/spec/rails_app/config/application.rb +6 -1
  84. data/spec/rails_app/config/cable.yml +8 -0
  85. data/spec/rails_app/config/dynamoid.rb +5 -0
  86. data/spec/rails_app/config/environment.rb +4 -1
  87. data/spec/rails_app/config/environments/production.rb +1 -1
  88. data/spec/rails_app/config/initializers/activity_notification.rb +30 -7
  89. data/spec/rails_app/config/locales/activity_notification.en.yml +2 -0
  90. data/spec/rails_app/db/seeds.rb +21 -5
  91. data/spec/rails_app/lib/mailer_previews/mailer_preview.rb +12 -4
  92. data/spec/roles/acts_as_notifiable_spec.rb +2 -2
  93. data/spec/roles/acts_as_target_spec.rb +1 -1
  94. data/spec/spec_helper.rb +15 -8
  95. metadata +67 -20
  96. data/spec/rails_app/app/models/.keep +0 -0
  97. data/spec/rails_app/app/views/activity_notification/notifications/users/overriden/custom/_test.html.erb +0 -1
@@ -52,7 +52,8 @@ module ActivityNotification
52
52
  if subscription_params[:subscribing] == false && subscription_params[:subscribing_to_email].nil?
53
53
  subscription_params[:subscribing_to_email] = subscription_params[:subscribing]
54
54
  end
55
- subscription = subscriptions.new(subscription_params)
55
+ subscription = Subscription.new(subscription_params)
56
+ subscription.assign_attributes(target: self)
56
57
  subscription.subscribing? ?
57
58
  subscription.assign_attributes(subscribing: true, subscribed_at: created_at) :
58
59
  subscription.assign_attributes(subscribing: false, unsubscribed_at: created_at)
@@ -65,11 +66,11 @@ module ActivityNotification
65
66
  optional_targets = subscription.subscribing_to_optional_target?(optional_target_name) ?
66
67
  optional_targets.merge(
67
68
  Subscription.to_optional_target_key(optional_target_name) => true,
68
- Subscription.to_optional_target_subscribed_at_key(optional_target_name) => created_at
69
+ Subscription.to_optional_target_subscribed_at_key(optional_target_name) => Subscription.convert_time_as_hash(created_at)
69
70
  ) :
70
71
  optional_targets.merge(
71
72
  Subscription.to_optional_target_key(optional_target_name) => false,
72
- Subscription.to_optional_target_unsubscribed_at_key(optional_target_name) => created_at
73
+ Subscription.to_optional_target_unsubscribed_at_key(optional_target_name) => Subscription.convert_time_as_hash(created_at)
73
74
  )
74
75
  end
75
76
  subscription.assign_attributes(optional_targets: optional_targets)
@@ -128,7 +129,7 @@ module ActivityNotification
128
129
  protected
129
130
 
130
131
  # Returns if the target subscribes to the notification.
131
- # This method can be overriden.
132
+ # This method can be overridden.
132
133
  # @api protected
133
134
  #
134
135
  # @param [String] key Key of the notification
@@ -139,7 +140,7 @@ module ActivityNotification
139
140
  end
140
141
 
141
142
  # Returns if the target subscribes to the notification email.
142
- # This method can be overriden.
143
+ # This method can be overridden.
143
144
  # @api protected
144
145
  #
145
146
  # @param [String] key Key of the notification
@@ -151,7 +152,7 @@ module ActivityNotification
151
152
  alias_method :_subscribes_to_email?, :_subscribes_to_notification_email?
152
153
 
153
154
  # Returns if the target subscribes to the specified optional target.
154
- # This method can be overriden.
155
+ # This method can be overridden.
155
156
  # @api protected
156
157
  #
157
158
  # @param [String] key Key of the notification
@@ -19,6 +19,8 @@ module ActivityNotification
19
19
  :_notification_email_allowed,
20
20
  :_batch_notification_email_allowed,
21
21
  :_notification_subscription_allowed,
22
+ :_notification_action_cable_allowed,
23
+ :_notification_action_cable_with_devise,
22
24
  :_notification_devise_resource,
23
25
  :_notification_current_devise_target,
24
26
  :_printable_notification_target_name
@@ -35,13 +37,15 @@ module ActivityNotification
35
37
  # Sets default values to target class fields.
36
38
  # @return [NilClass] nil
37
39
  def set_target_class_defaults
38
- self._notification_email = nil
39
- self._notification_email_allowed = ActivityNotification.config.email_enabled
40
- self._batch_notification_email_allowed = ActivityNotification.config.email_enabled
41
- self._notification_subscription_allowed = ActivityNotification.config.subscription_enabled
42
- self._notification_devise_resource = ->(model) { model }
43
- self._notification_current_devise_target = ->(current_resource) { current_resource }
44
- self._printable_notification_target_name = :printable_name
40
+ self._notification_email = nil
41
+ self._notification_email_allowed = ActivityNotification.config.email_enabled
42
+ self._batch_notification_email_allowed = ActivityNotification.config.email_enabled
43
+ self._notification_subscription_allowed = ActivityNotification.config.subscription_enabled
44
+ self._notification_action_cable_allowed = ActivityNotification.config.action_cable_enabled
45
+ self._notification_action_cable_with_devise = ActivityNotification.config.action_cable_with_devise
46
+ self._notification_devise_resource = ->(model) { model }
47
+ self._notification_current_devise_target = ->(current_resource) { current_resource }
48
+ self._printable_notification_target_name = :printable_name
45
49
  nil
46
50
  end
47
51
 
@@ -75,8 +79,8 @@ module ActivityNotification
75
79
  end
76
80
  target_notifications = target_notifications.limit(options[:limit]) if options[:limit].present?
77
81
  as_latest_group_member ?
78
- target_notifications.map{ |n| n.latest_group_member } :
79
- target_notifications.to_a
82
+ target_notifications.latest_order!(reverse).map{ |n| n.latest_group_member } :
83
+ target_notifications.latest_order!(reverse).to_a
80
84
  end
81
85
 
82
86
  # Gets all notifications for this target type grouped by targets.
@@ -133,7 +137,7 @@ module ActivityNotification
133
137
  end
134
138
 
135
139
  # Resolves current authenticated target by devise authentication from current resource signed in with Devise.
136
- # This method is able to be overriden.
140
+ # This method is able to be overridden.
137
141
  #
138
142
  # @param [Object] current_resource Current resource signed in with Devise
139
143
  # @return [Object] Current authenticated target by devise authentication
@@ -150,15 +154,15 @@ module ActivityNotification
150
154
  end
151
155
 
152
156
  # Returns target email address for email notification.
153
- # This method is able to be overriden.
157
+ # This method is able to be overridden.
154
158
  #
155
159
  # @return [String] Target email address
156
160
  def mailer_to
157
161
  resolve_value(_notification_email)
158
162
  end
159
163
 
160
- # Returns if sending notification email is allowed for the target from configured field or overriden method.
161
- # This method is able to be overriden.
164
+ # Returns if sending notification email is allowed for the target from configured field or overridden method.
165
+ # This method is able to be overridden.
162
166
  #
163
167
  # @param [Object] notifiable Notifiable instance of the notification
164
168
  # @param [String] key Key of the notification
@@ -167,8 +171,8 @@ module ActivityNotification
167
171
  resolve_value(_notification_email_allowed, notifiable, key)
168
172
  end
169
173
 
170
- # Returns if sending batch notification email is allowed for the target from configured field or overriden method.
171
- # This method is able to be overriden.
174
+ # Returns if sending batch notification email is allowed for the target from configured field or overridden method.
175
+ # This method is able to be overridden.
172
176
  #
173
177
  # @param [String] key Key of the notifications
174
178
  # @return [Boolean] If sending batch notification email is allowed for the target
@@ -176,8 +180,8 @@ module ActivityNotification
176
180
  resolve_value(_batch_notification_email_allowed, key)
177
181
  end
178
182
 
179
- # Returns if subscription management is allowed for the target from configured field or overriden method.
180
- # This method is able to be overriden.
183
+ # Returns if subscription management is allowed for the target from configured field or overridden method.
184
+ # This method is able to be overridden.
181
185
  #
182
186
  # @param [String] key Key of the notifications
183
187
  # @return [Boolean] If subscription management is allowed for the target
@@ -186,13 +190,54 @@ module ActivityNotification
186
190
  end
187
191
  alias_method :notification_subscription_allowed?, :subscription_allowed?
188
192
 
193
+ # Returns if publishing WebSocket using ActionCable is allowed for the target from configured field or overridden method.
194
+ # This method is able to be overridden.
195
+ #
196
+ # @param [Object] notifiable Notifiable instance of the notification
197
+ # @param [String] key Key of the notification
198
+ # @return [Boolean] If publishing WebSocket using ActionCable is allowed for the target
199
+ def notification_action_cable_allowed?(notifiable = nil, key = nil)
200
+ resolve_value(_notification_action_cable_allowed, notifiable, key)
201
+ end
202
+
203
+ # Returns if publishing WebSocket using ActionCable is allowed only for the authenticated target with Devise from configured field or overridden method.
204
+ #
205
+ # @return [Boolean] If publishing WebSocket using ActionCable is allowed for the target
206
+ def notification_action_cable_with_devise?
207
+ resolve_value(_notification_action_cable_with_devise)
208
+ end
209
+
210
+ # :only-rails5-plus#only-rails-with-callback-issue:
211
+ # :only-rails5-plus#only-rails-without-callback-issue:
212
+ # :only-rails5-plus#only-rails-with-callback-issue#except-dynamoid:
213
+ # :only-rails5-plus#only-rails-without-callback-issue#except-dynamoid:
214
+ if Rails::VERSION::MAJOR >= 5
215
+ # Returns notification ActionCable channel class name from action_cable_with_devise? configuration.
216
+ #
217
+ # @return [String] Notification ActionCable channel class name from action_cable_with_devise? configuration
218
+ def notification_action_cable_channel_class_name
219
+ notification_action_cable_with_devise? ? "ActivityNotification::NotificationWithDeviseChannel" : "ActivityNotification::NotificationChannel"
220
+ end
221
+ end
222
+ # :only-rails5-plus#only-rails-with-callback-issue:
223
+ # :only-rails5-plus#only-rails-without-callback-issue:
224
+ # :only-rails5-plus#only-rails-with-callback-issue#except-dynamoid:
225
+ # :only-rails5-plus#only-rails-without-callback-issue#except-dynamoid:
226
+
227
+ # Returns Devise resource model associated with this target.
228
+ #
229
+ # @return [Object] Devise resource model associated with this target
230
+ def notification_devise_resource
231
+ resolve_value(_notification_devise_resource)
232
+ end
233
+
189
234
  # Returns if current resource signed in with Devise is authenticated for the notification.
190
- # This method is able to be overriden.
235
+ # This method is able to be overridden.
191
236
  #
192
237
  # @param [Object] current_resource Current resource signed in with Devise
193
238
  # @return [Boolean] If current resource signed in with Devise is authenticated for the notification
194
239
  def authenticated_with_devise?(current_resource)
195
- devise_resource = resolve_value(_notification_devise_resource)
240
+ devise_resource = notification_devise_resource
196
241
  unless current_resource.blank? or current_resource.is_a? devise_resource.class
197
242
  raise TypeError,
198
243
  "Different type of current resource #{current_resource.class} "\
@@ -237,7 +282,7 @@ module ActivityNotification
237
282
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago])
238
283
  # @return [Boolean] If the target has unopened notifications
239
284
  def has_unopened_notifications?(options = {})
240
- _unopened_notification_index(options).present?
285
+ _unopened_notification_index(options).exists?
241
286
  end
242
287
 
243
288
  # Returns automatically arranged notification index of the target.
@@ -415,7 +460,7 @@ module ActivityNotification
415
460
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago])
416
461
  # @return [Array<Notificaion>] Unopened notification index of the target with attributes
417
462
  def unopened_notification_index_with_attributes(options = {})
418
- include_attributes _unopened_notification_index(options)
463
+ include_attributes(_unopened_notification_index(options)).to_a
419
464
  end
420
465
 
421
466
  # Gets opened notification index of the target with including attributes like target, notifiable, group and notifier.
@@ -436,7 +481,7 @@ module ActivityNotification
436
481
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago])
437
482
  # @return [Array<Notificaion>] Opened notification index of the target with attributes
438
483
  def opened_notification_index_with_attributes(options = {})
439
- include_attributes _opened_notification_index(options)
484
+ include_attributes(_opened_notification_index(options)).to_a
440
485
  end
441
486
 
442
487
  # Sends notification email to the target.
@@ -513,7 +558,7 @@ module ActivityNotification
513
558
  # @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
514
559
  # @option options [String] :filtered_by_key (nil) Key of the notification for filter
515
560
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago])
516
- # @return [ActiveRecord_AssociationRelation<Notificaion>] Unopened notification index of the target
561
+ # @return [ActiveRecord_AssociationRelation<Notificaion>|Mongoid::Criteria<Notificaion>|Dynamoid::Criteria::Chain] Unopened notification index of the target
517
562
  def _unopened_notification_index(options = {})
518
563
  reverse = options[:reverse] || false
519
564
  with_group_members = options[:with_group_members] || false
@@ -533,7 +578,7 @@ module ActivityNotification
533
578
  # @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
534
579
  # @option options [String] :filtered_by_key (nil) Key of the notification for filter
535
580
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ["created_at >= ?", time.hour.ago])
536
- # @return [Array<Notificaion>] Opened notification index of the target
581
+ # @return [ActiveRecord_AssociationRelation<Notificaion>|Mongoid::Criteria<Notificaion>|Dynamoid::Criteria::Chain] Opened notification index of the target
537
582
  def _opened_notification_index(options = {})
538
583
  limit = options[:limit] || ActivityNotification.config.opened_index_limit
539
584
  reverse = options[:reverse] || false
@@ -546,11 +591,11 @@ module ActivityNotification
546
591
  # Otherwise, target, notifiable and or notifier will be include without group.
547
592
  # @api private
548
593
  #
549
- # @param [ActiveRecord_AssociationRelation<Notificaion>] target_index Notification index
550
- # @return [ActiveRecord_AssociationRelation<Notificaion>] Notification index with attributes
594
+ # @param [ActiveRecord_AssociationRelation<Notificaion>|Mongoid::Criteria<Notificaion>|Dynamoid::Criteria::Chain] target_index Notification index
595
+ # @return [ActiveRecord_AssociationRelation<Notificaion>|Mongoid::Criteria<Notificaion>|Dynamoid::Criteria::Chain] Notification index with attributes
551
596
  def include_attributes(target_index)
552
597
  if target_index.present?
553
- Notification.group_member_exists?(target_index) ?
598
+ Notification.group_member_exists?(target_index.to_a) ?
554
599
  target_index.with_target.with_notifiable.with_group.with_notifier :
555
600
  target_index.with_target.with_notifiable.with_notifier
556
601
  else
@@ -606,7 +651,7 @@ module ActivityNotification
606
651
  if has_unopened_notifications?(options)
607
652
  # Return unopened notifications first
608
653
  target_unopened_index = arrange_single_notification_index(loading_unopened_index_method, options)
609
- # Total limit if notification index
654
+ # Total limit of notification index
610
655
  total_limit = options[:limit] || ActivityNotification.config.opened_index_limit
611
656
  # Additionaly, return opened notifications unless unopened index size overs the limit
612
657
  if (opened_limit = total_limit - target_unopened_index.size) > 0
@@ -28,13 +28,13 @@ module ActivityNotification
28
28
  self.class.name.demodulize.underscore.to_sym
29
29
  end
30
30
 
31
- # Initialize method to be overriden in user implementation class
31
+ # Initialize method to be overridden in user implementation class
32
32
  # @param [Hash] _options Options for initializing
33
33
  def initialize_target(_options = {})
34
34
  raise NotImplementedError, "You have to implement #{self.class}##{__method__}"
35
35
  end
36
36
 
37
- # Publishing notification method to be overriden in user implementation class
37
+ # Publishing notification method to be overridden in user implementation class
38
38
  # @param [Notification] _notification Notification instance
39
39
  # @param [Hash] _options Options for publishing
40
40
  def notify(_notification, _options = {})
@@ -8,9 +8,7 @@ module ActivityNotification
8
8
  include Common
9
9
  include Renderable
10
10
  include NotificationApi
11
- # @deprecated ActivityNotification.config.table_name as of 1.1.0
12
- self.table_name = ActivityNotification.config.table_name || ActivityNotification.config.notification_table_name
13
- # self.table_name = ActivityNotification.config.notification_table_name
11
+ self.table_name = ActivityNotification.config.notification_table_name
14
12
 
15
13
  # Belongs to target instance of this notification as polymorphic association.
16
14
  # @scope instance
@@ -152,27 +150,10 @@ module ActivityNotification
152
150
  # @return [ActiveRecord_AssociationRelation<Notificaion>] Database query of notifications with notifier
153
151
  scope :with_notifier, -> { includes(:notifier) }
154
152
 
155
- # Returns latest notification instance.
156
- # @return [Notification] Latest notification instance
157
- def self.latest
158
- latest_order.first
159
- end
160
-
161
- # Returns earliest notification instance.
162
- # @return [Notification] Earliest notification instance
163
- def self.earliest
164
- earliest_order.first
165
- end
166
-
167
- # Selects unique keys from query for notifications.
168
- # @return [Array<String>] Array of notification unique keys
169
- def self.uniq_keys
170
- # select method cannot be chained with order by other columns like created_at
171
- # select(:key).distinct.pluck(:key)
172
- pluck(:key).uniq
173
- end
174
-
175
153
  # Raise DeleteRestrictionError for notifications.
154
+ # @param [String] error_text Error text for raised exception
155
+ # @raise DeleteRestrictionError
156
+ # @return [void]
176
157
  def self.raise_delete_restriction_error(error_text)
177
158
  raise ::ActiveRecord::DeleteRestrictionError.new(error_text)
178
159
  end
@@ -0,0 +1,495 @@
1
+ require 'dynamoid/adapter_plugin/aws_sdk_v3'
2
+ require_relative 'dynamoid/extension.rb'
3
+
4
+ module ActivityNotification
5
+ module Association
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_associated_composite_records
10
+ self._associated_composite_records = []
11
+ end
12
+
13
+ class_methods do
14
+ # Defines has_many association with ActivityNotification models.
15
+ # @return [Dynamoid::Criteria::Chain] Database query of associated model instances
16
+ def has_many_records(name, options = {})
17
+ has_many_composite_xdb_records name, options
18
+ end
19
+
20
+ # Defines polymorphic belongs_to association using composite key with models in other database.
21
+ def belongs_to_composite_xdb_record(name, _options = {})
22
+ association_name = name.to_s.singularize.underscore
23
+ composite_field = "#{association_name}_key".to_sym
24
+ field composite_field, :string
25
+ associated_record_field = "#{association_name}_record".to_sym
26
+ field associated_record_field, :string if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
27
+
28
+ self.instance_eval do
29
+ define_method(name) do |reload = false|
30
+ reload and self.instance_variable_set("@#{name}", nil)
31
+ if self.instance_variable_get("@#{name}").blank?
32
+ composite_key = self.send(composite_field)
33
+ if composite_key.present? && (class_name = composite_key.split(ActivityNotification.config.composite_key_delimiter).first).present?
34
+ object_class = class_name.classify.constantize
35
+ self.instance_variable_set("@#{name}", object_class.where(id: composite_key.split(ActivityNotification.config.composite_key_delimiter).last).first)
36
+ end
37
+ end
38
+ self.instance_variable_get("@#{name}")
39
+ end
40
+
41
+ define_method("#{name}=") do |new_instance|
42
+ if new_instance.nil?
43
+ self.send("#{composite_field}=", nil)
44
+ else
45
+ self.send("#{composite_field}=", "#{new_instance.class.name}#{ActivityNotification.config.composite_key_delimiter}#{new_instance.id}")
46
+ self.send("#{associated_record_field}=", new_instance.to_json) if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
47
+ end
48
+ self.instance_variable_set("@#{name}", nil)
49
+ end
50
+
51
+ define_method("#{association_name}_type") do
52
+ composite_key = self.send(composite_field)
53
+ composite_key.present? ? composite_key.split(ActivityNotification.config.composite_key_delimiter).first : nil
54
+ end
55
+
56
+ define_method("#{association_name}_id") do
57
+ composite_key = self.send(composite_field)
58
+ composite_key.present? ? composite_key.split(ActivityNotification.config.composite_key_delimiter).last : nil
59
+ end
60
+ end
61
+
62
+ self._associated_composite_records.push(association_name.to_sym)
63
+ end
64
+
65
+ # Defines polymorphic has_many association using composite key with models in other database.
66
+ # @todo Add dependent option
67
+ def has_many_composite_xdb_records(name, options = {})
68
+ association_name = options[:as] || name.to_s.underscore
69
+ composite_field = "#{association_name}_key".to_sym
70
+ object_name = options[:class_name] || name.to_s.singularize.camelize
71
+ object_class = object_name.classify.constantize
72
+
73
+ self.instance_eval do
74
+ # Set default reload arg to true since Dynamoid::Criteria::Chain is stateful on the query
75
+ define_method(name) do |reload = true|
76
+ reload and self.instance_variable_set("@#{name}", nil)
77
+ if self.instance_variable_get("@#{name}").blank?
78
+ new_value = object_class.where(composite_field => "#{self.class.name}#{ActivityNotification.config.composite_key_delimiter}#{self.id}")
79
+ self.instance_variable_set("@#{name}", new_value)
80
+ end
81
+ self.instance_variable_get("@#{name}")
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ # Defines update method as update_attributes method
88
+ def update(attributes)
89
+ attributes_with_association = attributes.map { |attribute, value|
90
+ self.class._associated_composite_records.include?(attribute) ?
91
+ ["#{attribute}_key".to_sym, value.nil? ? nil : "#{value.class.name}#{ActivityNotification.config.composite_key_delimiter}#{value.id}"] :
92
+ [attribute, value]
93
+ }.to_h
94
+ update_attributes(attributes_with_association)
95
+ end
96
+ end
97
+ end
98
+
99
+ # Monkey patching for Rails 6.0+
100
+ class ActiveModel::NullMutationTracker
101
+ # Monkey patching for Rails 6.0+
102
+ def force_change(attr_name); end if Rails::VERSION::MAJOR >= 6
103
+ end
104
+
105
+ # Entend Dynamoid to support ActivityNotification scope in Dynamoid::Criteria::Chain
106
+ # @private
107
+ module Dynamoid # :nodoc: all
108
+ # https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/criteria.rb
109
+ # @private
110
+ module Criteria
111
+ # https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/criteria/chain.rb
112
+ # @private
113
+ class Chain
114
+ # Selects all notification index.
115
+ # ActivityNotification::Notification.all_index!
116
+ # is defined same as
117
+ # ActivityNotification::Notification.group_owners_only.latest_order
118
+ # @scope class
119
+ # @example Get all notification index of the @user
120
+ # @notifications = @user.notifications.all_index!
121
+ # @notifications = @user.notifications.group_owners_only.latest_order
122
+ # @param [Boolean] reverse If notification index will be ordered as earliest first
123
+ # @param [Boolean] with_group_members If notification index will include group members
124
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
125
+ def all_index!(reverse = false, with_group_members = false)
126
+ target_index = with_group_members ? self : group_owners_only
127
+ reverse ? target_index.earliest_order : target_index.latest_order
128
+ end
129
+
130
+ # Selects unopened notification index.
131
+ # ActivityNotification::Notification.unopened_index
132
+ # is defined same as
133
+ # ActivityNotification::Notification.unopened_only.group_owners_only.latest_order
134
+ # @scope class
135
+ # @example Get unopened notificaton index of the @user
136
+ # @notifications = @user.notifications.unopened_index
137
+ # @notifications = @user.notifications.unopened_only.group_owners_only.latest_order
138
+ # @param [Boolean] reverse If notification index will be ordered as earliest first
139
+ # @param [Boolean] with_group_members If notification index will include group members
140
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
141
+ def unopened_index(reverse = false, with_group_members = false)
142
+ target_index = with_group_members ? unopened_only : unopened_only.group_owners_only
143
+ reverse ? target_index.earliest_order : target_index.latest_order
144
+ end
145
+
146
+ # Selects unopened notification index.
147
+ # ActivityNotification::Notification.opened_index(limit)
148
+ # is defined same as
149
+ # ActivityNotification::Notification.opened_only(limit).group_owners_only.latest_order
150
+ # @scope class
151
+ # @example Get unopened notificaton index of the @user with limit 10
152
+ # @notifications = @user.notifications.opened_index(10)
153
+ # @notifications = @user.notifications.opened_only(10).group_owners_only.latest_order
154
+ # @param [Integer] limit Limit to query for opened notifications
155
+ # @param [Boolean] reverse If notification index will be ordered as earliest first
156
+ # @param [Boolean] with_group_members If notification index will include group members
157
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
158
+ def opened_index(limit, reverse = false, with_group_members = false)
159
+ target_index = with_group_members ? opened_only(limit) : opened_only(limit).group_owners_only
160
+ reverse ? target_index.earliest_order : target_index.latest_order
161
+ end
162
+
163
+ # Selects filtered notifications or subscriptions by associated instance.
164
+ # @scope class
165
+ # @param [String] name Association name
166
+ # @param [Object] instance Associated instance
167
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
168
+ def filtered_by_association(name, instance)
169
+ instance.present? ? where("#{name}_key" => "#{instance.class.name}#{ActivityNotification.config.composite_key_delimiter}#{instance.id}") : where("#{name}_key.null" => true)
170
+ end
171
+
172
+ # Selects filtered notifications or subscriptions by association type.
173
+ # @scope class
174
+ # @param [String] name Association name
175
+ # @param [Object] type Association type
176
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
177
+ def filtered_by_association_type(name, type)
178
+ type.present? ? where("#{name}_key.begins_with" => "#{type}#{ActivityNotification.config.composite_key_delimiter}") : none
179
+ end
180
+
181
+ # Selects filtered notifications or subscriptions by association type and id.
182
+ # @scope class
183
+ # @param [String] name Association name
184
+ # @param [Object] type Association type
185
+ # @param [String] id Association id
186
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
187
+ def filtered_by_association_type_and_id(name, type, id)
188
+ type.present? && id.present? ? where("#{name}_key" => "#{type}#{ActivityNotification.config.composite_key_delimiter}#{id}") : none
189
+ end
190
+
191
+ # Selects filtered notifications or subscriptions by target instance.
192
+ # ActivityNotification::Notification.filtered_by_target(@user)
193
+ # is the same as
194
+ # @user.notifications
195
+ # @scope class
196
+ # @param [Object] target Target instance for filter
197
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
198
+ def filtered_by_target(target)
199
+ filtered_by_association("target", target)
200
+ end
201
+
202
+ # Selects filtered notifications by notifiable instance.
203
+ # @example Get filtered unopened notificatons of the @user for @comment as notifiable
204
+ # @notifications = @user.notifications.unopened_only.filtered_by_instance(@comment)
205
+ # @scope class
206
+ # @param [Object] notifiable Notifiable instance for filter
207
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
208
+ def filtered_by_instance(notifiable)
209
+ filtered_by_association("notifiable", notifiable)
210
+ end
211
+
212
+ # Selects filtered notifications by group instance.
213
+ # @example Get filtered unopened notificatons of the @user for @article as group
214
+ # @notifications = @user.notifications.unopened_only.filtered_by_group(@article)
215
+ # @scope class
216
+ # @param [Object] group Group instance for filter
217
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
218
+ def filtered_by_group(group)
219
+ filtered_by_association("group", group)
220
+ end
221
+
222
+ # Selects filtered notifications or subscriptions by target_type.
223
+ # @example Get filtered unopened notificatons of User as target type
224
+ # @notifications = ActivityNotification.Notification.unopened_only.filtered_by_target_type('User')
225
+ # @scope class
226
+ # @param [String] target_type Target type for filter
227
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
228
+ def filtered_by_target_type(target_type)
229
+ filtered_by_association_type("target", target_type)
230
+ end
231
+
232
+ # Selects filtered notifications by notifiable_type.
233
+ # @example Get filtered unopened notificatons of the @user for Comment notifiable class
234
+ # @notifications = @user.notifications.unopened_only.filtered_by_type('Comment')
235
+ # @scope class
236
+ # @param [String] notifiable_type Notifiable type for filter
237
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
238
+ def filtered_by_type(notifiable_type)
239
+ filtered_by_association_type("notifiable", notifiable_type)
240
+ end
241
+
242
+ # Selects filtered notifications or subscriptions by key.
243
+ # @example Get filtered unopened notificatons of the @user with key 'comment.reply'
244
+ # @notifications = @user.notifications.unopened_only.filtered_by_key('comment.reply')
245
+ # @scope class
246
+ # @param [String] key Key of the notification for filter
247
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
248
+ def filtered_by_key(key)
249
+ where(key: key)
250
+ end
251
+
252
+ # Selects filtered notifications or subscriptions by notifiable_type, group or key with filter options.
253
+ # @example Get filtered unopened notificatons of the @user for Comment notifiable class
254
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_type: 'Comment' })
255
+ # @example Get filtered unopened notificatons of the @user for @article as group
256
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_group: @article })
257
+ # @example Get filtered unopened notificatons of the @user for Article instance id=1 as group
258
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_group_type: 'Article', filtered_by_group_id: '1' })
259
+ # @example Get filtered unopened notificatons of the @user with key 'comment.reply'
260
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_key: 'comment.reply' })
261
+ # @example Get filtered unopened notificatons of the @user for Comment notifiable class with key 'comment.reply'
262
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_type: 'Comment', filtered_by_key: 'comment.reply' })
263
+ # @example Get custom filtered notificatons of the @user
264
+ # @notifications = @user.notifications.unopened_only.filtered_by_options({ custom_filter: ["created_at >= ?", time.hour.ago] })
265
+ # @scope class
266
+ # @param [Hash] options Options for filter
267
+ # @option options [String] :filtered_by_type (nil) Notifiable type for filter
268
+ # @option options [Object] :filtered_by_group (nil) Group instance for filter
269
+ # @option options [String] :filtered_by_group_type (nil) Group type for filter, valid with :filtered_by_group_id
270
+ # @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
271
+ # @option options [String] :filtered_by_key (nil) Key of the notification for filter
272
+ # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ['created_at.gt': time.hour.ago])
273
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
274
+ def filtered_by_options(options = {})
275
+ options = ActivityNotification.cast_to_indifferent_hash(options)
276
+ filtered_notifications = self
277
+ if options.has_key?(:filtered_by_type)
278
+ filtered_notifications = filtered_notifications.filtered_by_type(options[:filtered_by_type])
279
+ end
280
+ if options.has_key?(:filtered_by_group)
281
+ filtered_notifications = filtered_notifications.filtered_by_group(options[:filtered_by_group])
282
+ end
283
+ if options.has_key?(:filtered_by_group_type) && options.has_key?(:filtered_by_group_id)
284
+ filtered_notifications = filtered_notifications.filtered_by_association_type_and_id("group", options[:filtered_by_group_type], options[:filtered_by_group_id])
285
+ end
286
+ if options.has_key?(:filtered_by_key)
287
+ filtered_notifications = filtered_notifications.filtered_by_key(options[:filtered_by_key])
288
+ end
289
+ if options.has_key?(:custom_filter)
290
+ filtered_notifications = filtered_notifications.where(options[:custom_filter])
291
+ end
292
+ filtered_notifications
293
+ end
294
+
295
+ # Orders by latest (newest) first as created_at: :desc.
296
+ # It uses sort key of Global Secondary Index in DynamoDB tables.
297
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications or subscriptions ordered by latest first
298
+ def latest_order
299
+ # order(created_at: :desc)
300
+ scan_index_forward(false)
301
+ end
302
+
303
+ # Orders by earliest (older) first as created_at: :asc.
304
+ # It uses sort key of Global Secondary Index in DynamoDB tables.
305
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications or subscriptions ordered by earliest first
306
+ def earliest_order
307
+ # order(created_at: :asc)
308
+ scan_index_forward(true)
309
+ end
310
+
311
+ # Orders by latest (newest) first as created_at: :desc and returns as array.
312
+ # @param [Boolean] reverse If notifications or subscriptions will be ordered as earliest first
313
+ # @return [Array] Array of notifications or subscriptions ordered by latest first
314
+ def latest_order!(reverse = false)
315
+ # order(created_at: :desc)
316
+ reverse ? earliest_order! : earliest_order!.reverse
317
+ end
318
+
319
+ # Orders by earliest (older) first as created_at: :asc and returns as array.
320
+ # It does not use sort key in DynamoDB tables.
321
+ # @return [Array] Array of notifications or subscriptions ordered by earliest first
322
+ def earliest_order!
323
+ # order(created_at: :asc)
324
+ all.to_a.sort_by {|n| n.created_at }
325
+ end
326
+
327
+ # Orders by latest (newest) first as subscribed_at: :desc.
328
+ # @return [Array] Array of subscriptions ordered by latest subscribed_at first
329
+ def latest_subscribed_order
330
+ # order(subscribed_at: :desc)
331
+ earliest_subscribed_order.reverse
332
+ end
333
+
334
+ # Orders by earliest (older) first as subscribed_at: :asc.
335
+ # @return [Array] Array of subscriptions ordered by earliest subscribed_at first
336
+ def earliest_subscribed_order
337
+ # order(subscribed_at: :asc)
338
+ all.to_a.sort_by {|n| n.subscribed_at }
339
+ end
340
+
341
+ # Orders by key name as key: :asc.
342
+ # @return [Array] Array of subscriptions ordered by key name
343
+ def key_order
344
+ # order(key: :asc)
345
+ all.to_a.sort_by {|n| n.key }
346
+ end
347
+
348
+ # Selects group owner notifications only.
349
+ # @scope class
350
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
351
+ def group_owners_only
352
+ where('group_owner_id.null': true)
353
+ end
354
+
355
+ # Selects group member notifications only.
356
+ # @scope class
357
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
358
+ def group_members_only
359
+ where('group_owner_id.not_null': true)
360
+ end
361
+
362
+ # Selects unopened notifications only.
363
+ # @scope class
364
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
365
+ def unopened_only
366
+ where('opened_at.null': true)
367
+ end
368
+
369
+ # Selects opened notifications only without limit.
370
+ # Be careful to get too many records with this method.
371
+ # @scope class
372
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
373
+ def opened_only!
374
+ where('opened_at.not_null': true)
375
+ end
376
+
377
+ # Selects opened notifications only with limit.
378
+ # @scope class
379
+ # @param [Integer] limit Limit to query for opened notifications
380
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
381
+ def opened_only(limit)
382
+ limit == 0 ? none : opened_only!.limit(limit)
383
+ end
384
+
385
+ # Selects group member notifications in unopened_index.
386
+ # @scope class
387
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
388
+ def unopened_index_group_members_only
389
+ group_owner_ids = unopened_index.map(&:id)
390
+ group_owner_ids.empty? ? none : where('group_owner_id.in': group_owner_ids)
391
+ end
392
+
393
+ # Selects group member notifications in opened_index.
394
+ # @scope class
395
+ # @param [Integer] limit Limit to query for opened notifications
396
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
397
+ def opened_index_group_members_only(limit)
398
+ group_owner_ids = opened_index(limit).map(&:id)
399
+ group_owner_ids.empty? ? none : where('group_owner_id.in': group_owner_ids)
400
+ end
401
+
402
+ # Selects notifications within expiration.
403
+ # @scope class
404
+ # @param [ActiveSupport::Duration] expiry_delay Expiry period of notifications
405
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
406
+ def within_expiration_only(expiry_delay)
407
+ where('created_at.gt': expiry_delay.ago)
408
+ end
409
+
410
+ # Selects group member notifications with specified group owner ids.
411
+ # @scope class
412
+ # @param [Array<String>] owner_ids Array of group owner ids
413
+ # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications
414
+ def group_members_of_owner_ids_only(owner_ids)
415
+ owner_ids.present? ? where('group_owner_id.in': owner_ids) : none
416
+ end
417
+
418
+ # Includes target instance with query for notifications or subscriptions.
419
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with target
420
+ def with_target
421
+ self
422
+ end
423
+
424
+ # Includes notifiable instance with query for notifications.
425
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with notifiable
426
+ def with_notifiable
427
+ self
428
+ end
429
+
430
+ # Includes group instance with query for notifications.
431
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with group
432
+ def with_group
433
+ self
434
+ end
435
+
436
+ # Includes group owner instances with query for notifications.
437
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with group owner
438
+ def with_group_owner
439
+ self
440
+ end
441
+
442
+ # Includes group member instances with query for notifications.
443
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with group members
444
+ def with_group_members
445
+ self
446
+ end
447
+
448
+ # Includes notifier instance with query for notifications.
449
+ # @return [Dynamoid::Criteria::Chain] Database query of notifications with notifier
450
+ def with_notifier
451
+ self
452
+ end
453
+
454
+ # Dummy reload method for test of notifications or subscriptions.
455
+ def reload
456
+ self
457
+ end
458
+
459
+ # Returns latest notification instance.
460
+ # @return [Notification] Latest notification instance
461
+ def latest
462
+ latest_order.first
463
+ end
464
+
465
+ # Returns earliest notification instance.
466
+ # @return [Notification] Earliest notification instance
467
+ def earliest
468
+ earliest_order.first
469
+ end
470
+
471
+ # Returns latest notification instance.
472
+ # It does not use sort key in DynamoDB tables.
473
+ # @return [Notification] Latest notification instance
474
+ def latest!
475
+ latest_order!.first
476
+ end
477
+
478
+ # Returns earliest notification instance.
479
+ # It does not use sort key in DynamoDB tables.
480
+ # @return [Notification] Earliest notification instance
481
+ def earliest!
482
+ earliest_order!.first
483
+ end
484
+
485
+ # Selects unique keys from query for notifications or subscriptions.
486
+ # @return [Array<String>] Array of notification unique keys
487
+ def uniq_keys
488
+ all.to_a.collect {|n| n.key }.uniq
489
+ end
490
+ end
491
+ end
492
+ end
493
+
494
+ require_relative 'dynamoid/notification.rb'
495
+ require_relative 'dynamoid/subscription.rb'