activity_notification 1.7.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'