activity_notification 2.0.0 → 2.1.4

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +22 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  4. data/.github/pull_request_template.md +13 -0
  5. data/.gitignore +10 -3
  6. data/.travis.yml +6 -5
  7. data/CHANGELOG.md +60 -0
  8. data/Gemfile +8 -3
  9. data/Procfile +1 -1
  10. data/README.md +153 -1510
  11. data/activity_notification.gemspec +4 -1
  12. data/app/channels/activity_notification/notification_api_channel.rb +12 -0
  13. data/app/channels/activity_notification/notification_api_with_devise_channel.rb +46 -0
  14. data/app/channels/activity_notification/notification_channel.rb +2 -2
  15. data/app/channels/activity_notification/notification_with_devise_channel.rb +2 -2
  16. data/app/controllers/activity_notification/apidocs_controller.rb +75 -0
  17. data/app/controllers/activity_notification/notifications_api_controller.rb +143 -0
  18. data/app/controllers/activity_notification/notifications_api_with_devise_controller.rb +7 -0
  19. data/app/controllers/activity_notification/notifications_controller.rb +79 -53
  20. data/app/controllers/activity_notification/subscriptions_api_controller.rb +197 -0
  21. data/app/controllers/activity_notification/subscriptions_api_with_devise_controller.rb +7 -0
  22. data/app/controllers/activity_notification/subscriptions_controller.rb +78 -69
  23. data/app/views/activity_notification/notifications/default/_default.html.erb +18 -18
  24. data/app/views/activity_notification/notifications/default/_default_without_grouping.html.erb +14 -14
  25. data/app/views/activity_notification/notifications/default/index.html.erb +6 -6
  26. data/app/views/activity_notification/optional_targets/default/action_cable_channel/_default.html.erb +176 -0
  27. data/app/views/activity_notification/subscriptions/default/_form.html.erb +1 -1
  28. data/app/views/activity_notification/subscriptions/default/_notification_keys.html.erb +3 -31
  29. data/app/views/activity_notification/subscriptions/default/_subscription.html.erb +7 -7
  30. data/app/views/activity_notification/subscriptions/default/index.html.erb +11 -7
  31. data/bin/deploy_on_heroku.sh +3 -1
  32. data/docs/CODE_OF_CONDUCT.md +76 -0
  33. data/docs/CONTRIBUTING.md +36 -0
  34. data/docs/Functions.md +1130 -0
  35. data/docs/Setup.md +801 -0
  36. data/docs/Testing.md +148 -0
  37. data/gemfiles/Gemfile.rails-4.2 +5 -1
  38. data/gemfiles/Gemfile.rails-5.0 +6 -1
  39. data/gemfiles/Gemfile.rails-5.1 +6 -1
  40. data/gemfiles/Gemfile.rails-5.2 +6 -1
  41. data/gemfiles/{Gemfile.rails-6.0.rc → Gemfile.rails-6.0} +6 -5
  42. data/lib/activity_notification.rb +13 -0
  43. data/lib/activity_notification/apis/notification_api.rb +37 -93
  44. data/lib/activity_notification/apis/subscription_api.rb +20 -8
  45. data/lib/activity_notification/apis/swagger.rb +6 -0
  46. data/lib/activity_notification/common.rb +4 -1
  47. data/lib/activity_notification/config.rb +41 -21
  48. data/lib/activity_notification/controllers/common_api_controller.rb +30 -0
  49. data/lib/activity_notification/controllers/common_controller.rb +45 -21
  50. data/lib/activity_notification/controllers/concerns/swagger/error_responses.rb +55 -0
  51. data/lib/activity_notification/controllers/concerns/swagger/notifications_api.rb +273 -0
  52. data/lib/activity_notification/controllers/concerns/swagger/notifications_parameters.rb +92 -0
  53. data/lib/activity_notification/controllers/concerns/swagger/subscriptions_api.rb +405 -0
  54. data/lib/activity_notification/controllers/concerns/swagger/subscriptions_parameters.rb +50 -0
  55. data/lib/activity_notification/controllers/devise_authentication_controller.rb +7 -6
  56. data/lib/activity_notification/gem_version.rb +14 -0
  57. data/lib/activity_notification/helpers/errors.rb +2 -0
  58. data/lib/activity_notification/helpers/view_helpers.rb +4 -0
  59. data/lib/activity_notification/mailers/helpers.rb +17 -10
  60. data/lib/activity_notification/models/concerns/notifiable.rb +31 -15
  61. data/lib/activity_notification/models/concerns/subscriber.rb +12 -1
  62. data/lib/activity_notification/models/concerns/swagger/error_schema.rb +36 -0
  63. data/lib/activity_notification/models/concerns/swagger/notification_schema.rb +209 -0
  64. data/lib/activity_notification/models/concerns/swagger/subscription_schema.rb +162 -0
  65. data/lib/activity_notification/models/concerns/target.rb +36 -10
  66. data/lib/activity_notification/models/notification.rb +1 -0
  67. data/lib/activity_notification/models/subscription.rb +1 -0
  68. data/lib/activity_notification/optional_targets/action_cable_api_channel.rb +69 -0
  69. data/lib/activity_notification/optional_targets/action_cable_channel.rb +68 -0
  70. data/lib/activity_notification/optional_targets/base.rb +7 -13
  71. data/lib/activity_notification/orm/active_record/notification.rb +17 -1
  72. data/lib/activity_notification/orm/active_record/subscription.rb +1 -1
  73. data/lib/activity_notification/orm/dynamoid.rb +38 -3
  74. data/lib/activity_notification/orm/dynamoid/extension.rb +79 -1
  75. data/lib/activity_notification/orm/dynamoid/notification.rb +49 -14
  76. data/lib/activity_notification/orm/dynamoid/subscription.rb +2 -2
  77. data/lib/activity_notification/orm/mongoid.rb +32 -3
  78. data/lib/activity_notification/orm/mongoid/notification.rb +24 -6
  79. data/lib/activity_notification/orm/mongoid/subscription.rb +1 -1
  80. data/lib/activity_notification/rails/routes.rb +132 -48
  81. data/lib/activity_notification/renderable.rb +13 -2
  82. data/lib/activity_notification/roles/acts_as_notifiable.rb +39 -20
  83. data/lib/activity_notification/version.rb +1 -1
  84. data/lib/generators/activity_notification/controllers_generator.rb +2 -1
  85. data/lib/generators/templates/activity_notification.rb +8 -0
  86. data/lib/generators/templates/controllers/notifications_api_controller.rb +31 -0
  87. data/lib/generators/templates/controllers/notifications_api_with_devise_controller.rb +31 -0
  88. data/lib/generators/templates/controllers/notifications_controller.rb +1 -37
  89. data/lib/generators/templates/controllers/notifications_with_devise_controller.rb +1 -45
  90. data/lib/generators/templates/controllers/subscriptions_api_controller.rb +61 -0
  91. data/lib/generators/templates/controllers/subscriptions_api_with_devise_controller.rb +61 -0
  92. data/lib/generators/templates/controllers/subscriptions_controller.rb +14 -37
  93. data/lib/generators/templates/controllers/subscriptions_with_devise_controller.rb +14 -45
  94. data/lib/generators/templates/models/README +8 -4
  95. data/lib/generators/templates/models/notification.rb +1 -1
  96. data/lib/generators/templates/models/subscription.rb +1 -1
  97. data/package.json +8 -0
  98. data/spec/channels/notification_api_channel_shared_examples.rb +59 -0
  99. data/spec/channels/notification_api_channel_spec.rb +51 -0
  100. data/spec/channels/notification_api_with_devise_channel_spec.rb +78 -0
  101. data/spec/concerns/apis/notification_api_spec.rb +38 -3
  102. data/spec/concerns/models/notifiable_spec.rb +82 -18
  103. data/spec/concerns/models/subscriber_spec.rb +13 -16
  104. data/spec/concerns/models/target_spec.rb +32 -0
  105. data/spec/concerns/renderable_spec.rb +2 -2
  106. data/spec/config_spec.rb +26 -15
  107. data/spec/controllers/controller_spec_utility.rb +136 -0
  108. data/spec/controllers/notifications_api_controller_shared_examples.rb +506 -0
  109. data/spec/controllers/notifications_api_controller_spec.rb +19 -0
  110. data/spec/controllers/notifications_api_with_devise_controller_spec.rb +60 -0
  111. data/spec/controllers/notifications_controller_shared_examples.rb +54 -79
  112. data/spec/controllers/notifications_controller_spec.rb +1 -2
  113. data/spec/controllers/notifications_with_devise_controller_spec.rb +3 -12
  114. data/spec/controllers/subscriptions_api_controller_shared_examples.rb +750 -0
  115. data/spec/controllers/subscriptions_api_controller_spec.rb +19 -0
  116. data/spec/controllers/subscriptions_api_with_devise_controller_spec.rb +60 -0
  117. data/spec/controllers/subscriptions_controller_shared_examples.rb +94 -121
  118. data/spec/controllers/subscriptions_controller_spec.rb +1 -2
  119. data/spec/controllers/subscriptions_with_devise_controller_spec.rb +3 -12
  120. data/spec/helpers/view_helpers_spec.rb +4 -11
  121. data/spec/mailers/mailer_spec.rb +41 -0
  122. data/spec/models/notification_spec.rb +17 -0
  123. data/spec/models/subscription_spec.rb +8 -13
  124. data/spec/optional_targets/action_cable_api_channel_spec.rb +37 -0
  125. data/spec/optional_targets/action_cable_channel_spec.rb +44 -0
  126. data/spec/optional_targets/amazon_sns_spec.rb +0 -2
  127. data/spec/optional_targets/slack_spec.rb +0 -2
  128. data/spec/rails_app/Rakefile +9 -0
  129. data/spec/rails_app/app/assets/config/manifest.js +3 -0
  130. data/spec/rails_app/app/assets/images/.keep +0 -0
  131. data/spec/rails_app/app/controllers/admins_controller.rb +21 -0
  132. data/spec/rails_app/app/controllers/application_controller.rb +1 -1
  133. data/spec/rails_app/app/controllers/articles_controller.rb +6 -3
  134. data/spec/rails_app/app/controllers/spa_controller.rb +7 -0
  135. data/spec/rails_app/app/controllers/users/notifications_controller.rb +0 -65
  136. data/spec/rails_app/app/controllers/users/notifications_with_devise_controller.rb +0 -73
  137. data/spec/rails_app/app/controllers/users/subscriptions_controller.rb +0 -77
  138. data/spec/rails_app/app/controllers/users/subscriptions_with_devise_controller.rb +0 -85
  139. data/spec/rails_app/app/controllers/users_controller.rb +26 -0
  140. data/spec/rails_app/app/javascript/App.vue +40 -0
  141. data/spec/rails_app/app/javascript/components/DeviseTokenAuth.vue +82 -0
  142. data/spec/rails_app/app/javascript/components/Top.vue +98 -0
  143. data/spec/rails_app/app/javascript/components/notifications/Index.vue +200 -0
  144. data/spec/rails_app/app/javascript/components/notifications/Notification.vue +133 -0
  145. data/spec/rails_app/app/javascript/components/notifications/NotificationContent.vue +122 -0
  146. data/spec/rails_app/app/javascript/components/subscriptions/Index.vue +279 -0
  147. data/spec/rails_app/app/javascript/components/subscriptions/NewSubscription.vue +112 -0
  148. data/spec/rails_app/app/javascript/components/subscriptions/NotificationKey.vue +141 -0
  149. data/spec/rails_app/app/javascript/components/subscriptions/Subscription.vue +226 -0
  150. data/spec/rails_app/app/javascript/config/development.js +5 -0
  151. data/spec/rails_app/app/javascript/config/environment.js +7 -0
  152. data/spec/rails_app/app/javascript/config/production.js +5 -0
  153. data/spec/rails_app/app/javascript/config/test.js +5 -0
  154. data/spec/rails_app/app/javascript/packs/application.js +18 -0
  155. data/spec/rails_app/app/javascript/packs/spa.js +14 -0
  156. data/spec/rails_app/app/javascript/router/index.js +73 -0
  157. data/spec/rails_app/app/javascript/store/index.js +37 -0
  158. data/spec/rails_app/app/models/admin.rb +16 -15
  159. data/spec/rails_app/app/models/article.rb +26 -21
  160. data/spec/rails_app/app/models/comment.rb +24 -71
  161. data/spec/rails_app/app/models/dummy/dummy_group.rb +8 -0
  162. data/spec/rails_app/app/models/dummy/dummy_notifiable_target.rb +8 -0
  163. data/spec/rails_app/app/models/user.rb +44 -20
  164. data/spec/rails_app/app/views/activity_notification/notifications/default/article/_update.html.erb +146 -0
  165. data/spec/rails_app/app/views/articles/index.html.erb +51 -7
  166. data/spec/rails_app/app/views/articles/show.html.erb +1 -1
  167. data/spec/rails_app/app/views/layouts/_header.html.erb +8 -10
  168. data/spec/rails_app/app/views/spa/index.html.erb +2 -0
  169. data/spec/rails_app/babel.config.js +72 -0
  170. data/spec/rails_app/bin/webpack +18 -0
  171. data/spec/rails_app/bin/webpack-dev-server +18 -0
  172. data/spec/rails_app/config/application.rb +18 -2
  173. data/spec/rails_app/config/dynamoid.rb +11 -3
  174. data/spec/rails_app/config/environment.rb +2 -1
  175. data/spec/rails_app/config/environments/development.rb +5 -0
  176. data/spec/rails_app/config/environments/production.rb +6 -0
  177. data/spec/rails_app/config/environments/test.rb +5 -0
  178. data/spec/rails_app/config/initializers/activity_notification.rb +11 -3
  179. data/spec/rails_app/config/initializers/copy_it.aws.rb.template +6 -0
  180. data/spec/rails_app/config/initializers/devise_token_auth.rb +55 -0
  181. data/spec/rails_app/config/initializers/mysql.rb +9 -0
  182. data/spec/rails_app/config/locales/activity_notification.en.yml +2 -2
  183. data/spec/rails_app/config/routes.rb +37 -1
  184. data/spec/rails_app/config/webpack/development.js +5 -0
  185. data/spec/rails_app/config/webpack/environment.js +7 -0
  186. data/spec/rails_app/config/webpack/loaders/vue.js +6 -0
  187. data/spec/rails_app/config/webpack/production.js +5 -0
  188. data/spec/rails_app/config/webpack/test.js +5 -0
  189. data/spec/rails_app/config/webpacker.yml +97 -0
  190. data/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb +10 -0
  191. data/spec/rails_app/db/schema.rb +4 -1
  192. data/spec/rails_app/db/seeds.rb +10 -2
  193. data/spec/rails_app/lib/custom_optional_targets/raise_error.rb +14 -0
  194. data/spec/rails_app/package.json +23 -0
  195. data/spec/rails_app/postcss.config.js +12 -0
  196. data/spec/roles/acts_as_group_spec.rb +0 -2
  197. data/spec/roles/acts_as_notifiable_spec.rb +6 -8
  198. data/spec/roles/acts_as_notifier_spec.rb +0 -2
  199. data/spec/roles/acts_as_target_spec.rb +0 -4
  200. data/spec/spec_helper.rb +7 -15
  201. data/spec/version_spec.rb +31 -0
  202. metadata +191 -13
@@ -3,22 +3,11 @@ module ActivityNotification
3
3
  module OptionalTarget
4
4
  # Abstract optional target class to develop optional notification target class.
5
5
  class Base
6
- # View context to render notification message
7
- # @return View context to render notification message
8
- attr_accessor :view_context
9
-
10
6
  # Initialize method to create view context in this OptionalTarget instance
11
7
  # @param [Hash] options Options for initializing target
12
8
  # @option options [Boolean] :skip_initializing_target (false) Whether skip calling initialize_target method
13
9
  # @option options [Hash] others Options for initializing target
14
10
  def initialize(options = {})
15
- @view_context = ActionView::Base.new(ActionController::Base.view_paths, {})
16
- @view_context.class_eval do
17
- include Rails.application.routes.url_helpers
18
- def default_url_options
19
- ActionMailer::Base.default_url_options
20
- end
21
- end
22
11
  initialize_target(options) unless options.delete(:skip_initializing_target)
23
12
  end
24
13
 
@@ -64,12 +53,17 @@ module ActivityNotification
64
53
  "activity_notification/optional_targets/default/base"
65
54
  ]
66
55
  options[:fallback] ||= :default
67
- @view_context.assign((options[:assignment] || {}).merge(notification: notification, target: notification.target))
68
56
 
69
57
  message, missing_template = nil, nil
70
58
  partial_root_list.each do |partial_root|
71
59
  begin
72
- message = notification.render(@view_context, options.merge(partial_root: partial_root)).to_str
60
+ message = notification.render(
61
+ ActivityNotification::NotificationsController.renderer,
62
+ options.merge(
63
+ partial_root: partial_root,
64
+ assigns: (options[:assignment] || {}).merge(notification: notification, target: notification.target)
65
+ )
66
+ ).to_s
73
67
  break
74
68
  rescue ActionView::MissingTemplate => e
75
69
  missing_template = e
@@ -126,6 +126,22 @@ module ActivityNotification
126
126
  # @return [ActiveRecord_AssociationRelation<Notificaion>] Database query of filtered notifications
127
127
  scope :filtered_by_group, ->(group) { where(group: group) }
128
128
 
129
+ # Selects filtered notifications later than specified time.
130
+ # @example Get filtered unopened notificatons of the @user later than @notification
131
+ # @notifications = @user.notifications.unopened_only.later_than(@notification.created_at)
132
+ # @scope class
133
+ # @param [Time] Created time of the notifications for filter
134
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
135
+ scope :later_than, ->(created_time) { where('created_at > ?', created_time) }
136
+
137
+ # Selects filtered notifications earlier than specified time.
138
+ # @example Get filtered unopened notificatons of the @user earlier than @notification
139
+ # @notifications = @user.notifications.unopened_only.earlier_than(@notification.created_at)
140
+ # @scope class
141
+ # @param [Time] Created time of the notifications for filter
142
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
143
+ scope :earlier_than, ->(created_time) { where('created_at < ?', created_time) }
144
+
129
145
  # Includes target instance with query for notifications.
130
146
  # @return [ActiveRecord_AssociationRelation<Notificaion>] Database query of notifications with target
131
147
  scope :with_target, -> { includes(:target) }
@@ -152,7 +168,7 @@ module ActivityNotification
152
168
 
153
169
  # Raise DeleteRestrictionError for notifications.
154
170
  # @param [String] error_text Error text for raised exception
155
- # @raise DeleteRestrictionError
171
+ # @raise [ActiveRecord::DeleteRestrictionError] DeleteRestrictionError from used ORM
156
172
  # @return [void]
157
173
  def self.raise_delete_restriction_error(error_text)
158
174
  raise ::ActiveRecord::DeleteRestrictionError.new(error_text)
@@ -17,7 +17,7 @@ module ActivityNotification
17
17
  serialize :optional_targets, Hash
18
18
 
19
19
  validates :target, presence: true
20
- validates :key, presence: true
20
+ validates :key, presence: true, uniqueness: { scope: :target }
21
21
  validates_inclusion_of :subscribing, in: [true, false]
22
22
  validates_inclusion_of :subscribing_to_email, in: [true, false]
23
23
  validate :subscribing_to_email_cannot_be_true_when_subscribing_is_false
@@ -22,8 +22,8 @@ module ActivityNotification
22
22
  association_name = name.to_s.singularize.underscore
23
23
  composite_field = "#{association_name}_key".to_sym
24
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]
25
+ associated_record_field = "stored_#{association_name}".to_sym
26
+ field associated_record_field, :raw if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
27
27
 
28
28
  self.instance_eval do
29
29
  define_method(name) do |reload = false|
@@ -43,7 +43,14 @@ module ActivityNotification
43
43
  self.send("#{composite_field}=", nil)
44
44
  else
45
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]
46
+ associated_record_json = new_instance.as_json(_options[:as_json_options] || {})
47
+ # Cast Time and DateTime field to String to handle Dynamoid unsupported type error
48
+ if associated_record_json.present?
49
+ associated_record_json.each do |k, v|
50
+ associated_record_json[k] = v.to_s if v.is_a?(Time) || v.is_a?(DateTime)
51
+ end
52
+ end
53
+ self.send("#{associated_record_field}=", associated_record_json) if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
47
54
  end
48
55
  self.instance_variable_set("@#{name}", nil)
49
56
  end
@@ -249,6 +256,26 @@ module Dynamoid # :nodoc: all
249
256
  where(key: key)
250
257
  end
251
258
 
259
+ # Selects filtered notifications later than specified time.
260
+ # @example Get filtered unopened notificatons of the @user later than @notification
261
+ # @notifications = @user.notifications.unopened_only.later_than(@notification.created_at)
262
+ # @scope class
263
+ # @param [Time] Created time of the notifications for filter
264
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
265
+ def later_than(created_time)
266
+ where('created_at.gt': created_time)
267
+ end
268
+
269
+ # Selects filtered notifications earlier than specified time.
270
+ # @example Get filtered unopened notificatons of the @user earlier than @notification
271
+ # @notifications = @user.notifications.unopened_only.earlier_than(@notification.created_at)
272
+ # @scope class
273
+ # @param [Time] Created time of the notifications for filter
274
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
275
+ def earlier_than(created_time)
276
+ where('created_at.lt': created_time)
277
+ end
278
+
252
279
  # Selects filtered notifications or subscriptions by notifiable_type, group or key with filter options.
253
280
  # @example Get filtered unopened notificatons of the @user for Comment notifiable class
254
281
  # @notifications = @user.notifications.unopened_only.filtered_by_options({ filtered_by_type: 'Comment' })
@@ -269,6 +296,8 @@ module Dynamoid # :nodoc: all
269
296
  # @option options [String] :filtered_by_group_type (nil) Group type for filter, valid with :filtered_by_group_id
270
297
  # @option options [String] :filtered_by_group_id (nil) Group instance id for filter, valid with :filtered_by_group_type
271
298
  # @option options [String] :filtered_by_key (nil) Key of the notification for filter
299
+ # @option options [String] :later_than (nil) ISO 8601 format time to filter notification index later than specified time
300
+ # @option options [String] :earlier_than (nil) ISO 8601 format time to filter notification index earlier than specified time
272
301
  # @option options [Array|Hash] :custom_filter (nil) Custom notification filter (e.g. ['created_at.gt': time.hour.ago])
273
302
  # @return [Dynamoid::Criteria::Chain] Database query of filtered notifications or subscriptions
274
303
  def filtered_by_options(options = {})
@@ -286,6 +315,12 @@ module Dynamoid # :nodoc: all
286
315
  if options.has_key?(:filtered_by_key)
287
316
  filtered_notifications = filtered_notifications.filtered_by_key(options[:filtered_by_key])
288
317
  end
318
+ if options.has_key?(:later_than)
319
+ filtered_notifications = filtered_notifications.later_than(Time.iso8601(options[:later_than]))
320
+ end
321
+ if options.has_key?(:earlier_than)
322
+ filtered_notifications = filtered_notifications.earlier_than(Time.iso8601(options[:earlier_than]))
323
+ end
289
324
  if options.has_key?(:custom_filter)
290
325
  filtered_notifications = filtered_notifications.where(options[:custom_filter])
291
326
  end
@@ -1,6 +1,6 @@
1
1
  require 'dynamoid/adapter_plugin/aws_sdk_v3'
2
2
 
3
- # Entend Dynamoid to support none, limit, exists?, update_all in Dynamoid::Criteria::Chain.
3
+ # Entend Dynamoid v3.1.0 to support none, limit, exists?, update_all, serializable_hash in Dynamoid::Criteria::Chain.
4
4
  # ActivityNotification project will try to contribute these fundamental functions to Dynamoid upstream.
5
5
  # @private
6
6
  module Dynamoid # :nodoc: all
@@ -32,6 +32,7 @@ module Dynamoid # :nodoc: all
32
32
  # https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/criteria/chain.rb
33
33
  # @private
34
34
  class Chain
35
+ # Return new none object
35
36
  def none
36
37
  None.new(self.source)
37
38
  end
@@ -64,6 +65,11 @@ module Dynamoid # :nodoc: all
64
65
  document.update_attributes(conditions)
65
66
  end
66
67
  end
68
+
69
+ # Return serializable_hash as array
70
+ def serializable_hash(options = {})
71
+ all.to_a.map { |r| r.serializable_hash(options) }
72
+ end
67
73
  end
68
74
 
69
75
  # https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/criteria.rb
@@ -163,6 +169,78 @@ module Dynamoid # :nodoc: all
163
169
  end
164
170
  end
165
171
 
172
+ # Entend Dynamoid to support uniqueness validator
173
+ # @private
174
+ module Dynamoid # :nodoc: all
175
+ # https://github.com/Dynamoid/dynamoid/blob/master/lib/dynamoid/validations.rb
176
+ # @private
177
+ module Validations
178
+ # Validates whether or not a field is unique against the records in the database.
179
+ class UniquenessValidator < ActiveModel::EachValidator
180
+ # Validate the document for uniqueness violations.
181
+ # @param [Document] document The document to validate.
182
+ # @param [Symbol] attribute The name of the attribute.
183
+ # @param [Object] value The value of the object.
184
+ def validate_each(document, attribute, value)
185
+ return unless validation_required?(document, attribute)
186
+ document.errors.add(attribute, :taken, options.except(:scope).merge(value: value)) if not_unique?(document, attribute, value)
187
+ end
188
+
189
+ private
190
+
191
+ # Are we required to validate the document?
192
+ # @api private
193
+ def validation_required?(document, attribute)
194
+ document.new_record? ||
195
+ document.send("attribute_changed?", attribute.to_s) ||
196
+ scope_value_changed?(document)
197
+ end
198
+
199
+ # Scope reference has changed?
200
+ # @api private
201
+ def scope_value_changed?(document)
202
+ Array.wrap(options[:scope]).any? do |item|
203
+ document.send("attribute_changed?", item.to_s)
204
+ end
205
+ end
206
+
207
+ # Check whether a record is uniqueness.
208
+ # @api private
209
+ def not_unique?(document, attribute, value)
210
+ klass = document.class
211
+ while klass.superclass.respond_to?(:validators) && klass.superclass.validators.include?(self)
212
+ klass = klass.superclass
213
+ end
214
+ criteria = create_criteria(klass, document, attribute, value)
215
+ criteria.exists?
216
+ end
217
+
218
+ # Create the validation criteria.
219
+ # @api private
220
+ def create_criteria(base, document, attribute, value)
221
+ criteria = scope(base, document)
222
+ filter_criteria(criteria, document, attribute)
223
+ end
224
+
225
+ # Scope the criteria to the scope options provided.
226
+ # @api private
227
+ def scope(criteria, document)
228
+ Array.wrap(options[:scope]).each do |item|
229
+ criteria = filter_criteria(criteria, document, item)
230
+ end
231
+ criteria
232
+ end
233
+
234
+ # Filter the criteria.
235
+ # @api private
236
+ def filter_criteria(criteria, document, attribute)
237
+ value = document.read_attribute(attribute)
238
+ value.nil? ? criteria.where("#{attribute}.null" => true) : criteria.where(attribute => value)
239
+ end
240
+ end
241
+ end
242
+ end
243
+
166
244
  module ActivityNotification
167
245
  # Dynamoid extension module for ActivityNotification.
168
246
  module DynamoidExtension
@@ -20,17 +20,17 @@ module ActivityNotification
20
20
  # Belongs to target instance of this notification as polymorphic association using composite key.
21
21
  # @scope instance
22
22
  # @return [Object] Target instance of this notification
23
- belongs_to_composite_xdb_record :target, store_with_associated_records: true
23
+ belongs_to_composite_xdb_record :target, store_with_associated_records: true, as_json_options: { methods: [:printable_type, :printable_target_name] }
24
24
 
25
25
  # Belongs to notifiable instance of this notification as polymorphic association using composite key.
26
26
  # @scope instance
27
27
  # @return [Object] Notifiable instance of this notification
28
- belongs_to_composite_xdb_record :notifiable, store_with_associated_records: true
28
+ belongs_to_composite_xdb_record :notifiable, store_with_associated_records: true, as_json_options: { methods: [:printable_type] }
29
29
 
30
30
  # Belongs to group instance of this notification as polymorphic association using composite key.
31
31
  # @scope instance
32
32
  # @return [Object] Group instance of this notification
33
- belongs_to_composite_xdb_record :group
33
+ belongs_to_composite_xdb_record :group, store_with_associated_records: true, as_json_options: { methods: [:printable_type, :printable_group_name] }
34
34
 
35
35
  field :key, :string
36
36
  field :parameters, :raw, default: {}
@@ -64,14 +64,54 @@ module ActivityNotification
64
64
  # Belongs to :otifier instance of this notification.
65
65
  # @scope instance
66
66
  # @return [Object] Notifier instance of this notification
67
- belongs_to_composite_xdb_record :notifier, store_with_associated_records: true
67
+ belongs_to_composite_xdb_record :notifier, store_with_associated_records: true, as_json_options: { methods: [:printable_type, :printable_notifier_name] }
68
+
69
+ # Additional fields to store from instance method when config.store_with_associated_records is enabled
70
+ if ActivityNotification.config.store_with_associated_records
71
+ field :stored_notifiable_path, :string
72
+ field :stored_printable_notifiable_name, :string
73
+ field :stored_group_member_notifier_count, :integer
74
+ field :stored_group_notification_count, :integer
75
+ field :stored_group_members, :array
76
+
77
+ # Returns prepared notification object to store
78
+ # @return [Object] prepared notification object to store
79
+ def prepare_to_store
80
+ self.stored_notifiable_path = notifiable_path
81
+ self.stored_printable_notifiable_name = printable_notifiable_name
82
+ if group_owner?
83
+ self.stored_group_notification_count = 0
84
+ self.stored_group_member_notifier_count = 0
85
+ self.stored_group_members = []
86
+ end
87
+ self
88
+ end
89
+
90
+ # Call after store action with stored notification
91
+ def after_store
92
+ if group_owner?
93
+ self.stored_group_notification_count = group_notification_count
94
+ self.stored_group_member_notifier_count = group_member_notifier_count
95
+ self.stored_group_members = group_members.as_json
96
+ self.stored_group_members.each do |group_member|
97
+ # Cast Time and DateTime field to String to handle Dynamoid unsupported type error
98
+ group_member.each do |k, v|
99
+ group_member[k] = v.to_s if v.is_a?(Time) || v.is_a?(DateTime)
100
+ end
101
+ end
102
+ save
103
+ else
104
+ group_owner.after_store
105
+ end
106
+ end
107
+ end
68
108
 
69
109
  # Mandatory global secondary index to query effectively
70
- global_secondary_index hash_key: :target_key, range_key: :created_at, projected_attributes: :all
71
- global_secondary_index hash_key: :group_owner_id, range_key: :created_at, projected_attributes: :all
110
+ global_secondary_index name: :index_target_key_created_at, hash_key: :target_key, range_key: :created_at, projected_attributes: :all
111
+ global_secondary_index name: :index_group_owner_id_created_at, hash_key: :group_owner_id, range_key: :created_at, projected_attributes: :all
72
112
  # Optional global secondary index to sort by created_at
73
- global_secondary_index hash_key: :notifier_key, range_key: :created_at, projected_attributes: :all
74
- global_secondary_index hash_key: :notifiable_key, range_key: :created_at, projected_attributes: :all
113
+ global_secondary_index name: :index_notifier_key_created_at, hash_key: :notifier_key, range_key: :created_at, projected_attributes: :all
114
+ global_secondary_index name: :index_notifiable_key_created_at, hash_key: :notifiable_key, range_key: :created_at, projected_attributes: :all
75
115
 
76
116
  validates :target, presence: true
77
117
  validates :notifiable, presence: true
@@ -116,17 +156,12 @@ module ActivityNotification
116
156
 
117
157
  # Raise ActivityNotification::DeleteRestrictionError for notifications.
118
158
  # @param [String] error_text Error text for raised exception
119
- # @raise ActivityNotification::DeleteRestrictionError
159
+ # @raise [ActivityNotification::DeleteRestrictionError] DeleteRestrictionError from used ORM
120
160
  # @return [void]
121
161
  def self.raise_delete_restriction_error(error_text)
122
162
  raise ActivityNotification::DeleteRestrictionError, error_text
123
163
  end
124
164
 
125
- # Returns prepared notification object to store
126
- # @return [Object] prepared notification object to store
127
- # def prepare_to_store
128
- # end
129
-
130
165
  protected
131
166
 
132
167
  # Returns count of group members of the unopened notification.
@@ -28,10 +28,10 @@ module ActivityNotification
28
28
  field :unsubscribed_to_email_at, :datetime
29
29
  field :optional_targets, :raw, default: {}
30
30
 
31
- global_secondary_index hash_key: :target_key, range_key: :created_at, projected_attributes: :all
31
+ global_secondary_index name: :index_target_key_created_at, hash_key: :target_key, range_key: :created_at, projected_attributes: :all
32
32
 
33
33
  validates :target, presence: true
34
- validates :key, presence: true
34
+ validates :key, presence: true, uniqueness: { scope: :target_key }
35
35
  validates_inclusion_of :subscribing, in: [true, false]
36
36
  validates_inclusion_of :subscribing_to_email, in: [true, false]
37
37
  validate :subscribing_to_email_cannot_be_true_when_subscribing_is_false
@@ -24,8 +24,8 @@ module ActivityNotification
24
24
  id_field, type_field = "#{association_name}_id", "#{association_name}_type"
25
25
  field id_field, type: String
26
26
  field type_field, type: String
27
- associated_record_field = "#{association_name}_record"
28
- field associated_record_field, type: String if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
27
+ associated_record_field = "stored_#{association_name}"
28
+ field associated_record_field, type: Hash if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
29
29
 
30
30
  self.instance_eval do
31
31
  define_method(name) do |reload = false|
@@ -43,7 +43,14 @@ module ActivityNotification
43
43
  if new_instance.nil? then instance_id, instance_type = nil, nil else instance_id, instance_type = new_instance.id, new_instance.class.name end
44
44
  self.send("#{id_field}=", instance_id)
45
45
  self.send("#{type_field}=", instance_type)
46
- self.send("#{associated_record_field}=", new_instance.to_json) if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
46
+ associated_record_json = new_instance.as_json(_options[:as_json_options] || {})
47
+ # Cast Hash $oid field to String id to handle BSON::String::IllegalKey
48
+ if associated_record_json.present?
49
+ associated_record_json.each do |k, v|
50
+ associated_record_json[k] = v['$oid'] if v.is_a?(Hash) && v.has_key?('$oid')
51
+ end
52
+ end
53
+ self.send("#{associated_record_field}=", associated_record_json) if ActivityNotification.config.store_with_associated_records && _options[:store_with_associated_records]
47
54
  self.instance_variable_set("@#{name}", nil)
48
55
  end
49
56
  end
@@ -72,5 +79,27 @@ module ActivityNotification
72
79
  end
73
80
  end
74
81
 
82
+ # Monkey patching for Mongoid::Document as_json
83
+ module Mongoid
84
+ # Monkey patching for Mongoid::Document as_json
85
+ module Document
86
+ # Monkey patching for Mongoid::Document as_json
87
+ # @param [Hash] options Options parameter
88
+ # @return [Hash] Hash representing the model
89
+ def as_json(options = {})
90
+ json = super(options)
91
+ json["id"] = json["_id"].to_s.start_with?("{\"$oid\"=>") ? self.id.to_s : json["_id"].to_s
92
+ if options.has_key?(:include)
93
+ case options[:include]
94
+ when Symbol then json[options[:include].to_s] = self.send(options[:include]).as_json
95
+ when Array then options[:include].each {|model| json[model.to_s] = self.send(model).as_json }
96
+ when Hash then options[:include].each {|model, options| json[model.to_s] = self.send(model).as_json(options) }
97
+ end
98
+ end
99
+ json
100
+ end
101
+ end
102
+ end
103
+
75
104
  require_relative 'mongoid/notification.rb'
76
105
  require_relative 'mongoid/subscription.rb'
@@ -19,17 +19,17 @@ module ActivityNotification
19
19
  # Belongs to target instance of this notification as polymorphic association.
20
20
  # @scope instance
21
21
  # @return [Object] Target instance of this notification
22
- belongs_to_polymorphic_xdb_record :target, store_with_associated_records: true
22
+ belongs_to_polymorphic_xdb_record :target, store_with_associated_records: true, as_json_options: { methods: [:printable_type, :printable_target_name] }
23
23
 
24
24
  # Belongs to notifiable instance of this notification as polymorphic association.
25
25
  # @scope instance
26
26
  # @return [Object] Notifiable instance of this notification
27
- belongs_to_polymorphic_xdb_record :notifiable, store_with_associated_records: true
27
+ belongs_to_polymorphic_xdb_record :notifiable, store_with_associated_records: true, as_json_options: { methods: [:printable_type] }
28
28
 
29
29
  # Belongs to group instance of this notification as polymorphic association.
30
30
  # @scope instance
31
31
  # @return [Object] Group instance of this notification
32
- belongs_to_polymorphic_xdb_record :group
32
+ belongs_to_polymorphic_xdb_record :group, as_json_options: { methods: [:printable_type, :printable_group_name] }
33
33
 
34
34
  field :key, type: String
35
35
  field :parameters, type: Hash, default: {}
@@ -53,7 +53,7 @@ module ActivityNotification
53
53
  # Belongs to :otifier instance of this notification.
54
54
  # @scope instance
55
55
  # @return [Object] Notifier instance of this notification
56
- belongs_to_polymorphic_xdb_record :notifier, store_with_associated_records: true
56
+ belongs_to_polymorphic_xdb_record :notifier, store_with_associated_records: true, as_json_options: { methods: [:printable_type, :printable_notifier_name] }
57
57
 
58
58
  validates :target, presence: true
59
59
  validates :notifiable, presence: true
@@ -140,9 +140,27 @@ module ActivityNotification
140
140
  scope :filtered_by_group, ->(group) {
141
141
  group.present? ?
142
142
  where(group_id: group.id, group_type: group.class.name) :
143
- any_of({ :group_id.exists => false, :group_type.exists => false }, { group_id: nil, group_type: nil })
143
+ Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.1.0') ?
144
+ where(:group_id.exists => false, :group_type.exists => false).or(group_id: nil, group_type: nil) :
145
+ any_of({ :group_id.exists => false, :group_type.exists => false }, { group_id: nil, group_type: nil })
144
146
  }
145
147
 
148
+ # Selects filtered notifications later than specified time.
149
+ # @example Get filtered unopened notificatons of the @user later than @notification
150
+ # @notifications = @user.notifications.unopened_only.later_than(@notification.created_at)
151
+ # @scope class
152
+ # @param [Time] Created time of the notifications for filter
153
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
154
+ scope :later_than, ->(created_time) { where(:created_at.gt => created_time) }
155
+
156
+ # Selects filtered notifications earlier than specified time.
157
+ # @example Get filtered unopened notificatons of the @user earlier than @notification
158
+ # @notifications = @user.notifications.unopened_only.earlier_than(@notification.created_at)
159
+ # @scope class
160
+ # @param [Time] Created time of the notifications for filter
161
+ # @return [ActiveRecord_AssociationRelation<Notificaion>, Mongoid::Criteria<Notificaion>] Database query of filtered notifications
162
+ scope :earlier_than, ->(created_time) { where(:created_at.lt => created_time) }
163
+
146
164
  # Includes target instance with query for notifications.
147
165
  # @return [Mongoid::Criteria<Notificaion>] Database query of notifications with target
148
166
  scope :with_target, -> { }
@@ -179,7 +197,7 @@ module ActivityNotification
179
197
 
180
198
  # Raise ActivityNotification::DeleteRestrictionError for notifications.
181
199
  # @param [String] error_text Error text for raised exception
182
- # @raise ActivityNotification::DeleteRestrictionError
200
+ # @raise [ActivityNotification::DeleteRestrictionError] DeleteRestrictionError from used ORM
183
201
  # @return [void]
184
202
  def self.raise_delete_restriction_error(error_text)
185
203
  raise ActivityNotification::DeleteRestrictionError, error_text