activity_notification 2.0.0 → 2.1.4

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