activity_notification 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/app/jobs/activity_notification/cascading_notification_job.rb +123 -0
- data/docs/Functions.md +197 -1
- data/lib/activity_notification/apis/cascading_notification_api.rb +208 -0
- data/lib/activity_notification/apis/notification_api.rb +3 -0
- data/lib/activity_notification/config.rb +10 -0
- data/lib/activity_notification/mailers/helpers.rb +27 -1
- data/lib/activity_notification/version.rb +1 -1
- data/lib/generators/templates/activity_notification.rb +8 -0
- metadata +5 -441
- data/.codeclimate.yml +0 -33
- data/.coveralls.yml +0 -1
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -22
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- data/.github/pull_request_template.md +0 -13
- data/.github/workflows/build.yml +0 -100
- data/.gitignore +0 -74
- data/.rspec +0 -3
- data/.rubocop.yml +0 -1157
- data/.yardopts +0 -6
- data/CHANGELOG.md +0 -452
- data/Gemfile +0 -31
- data/Procfile +0 -2
- data/Rakefile +0 -28
- data/activity_notification.gemspec +0 -44
- data/ai-curated-specs/issues/172/design.md +0 -220
- data/ai-curated-specs/issues/172/tasks.md +0 -326
- data/ai-curated-specs/issues/188/design.md +0 -227
- data/ai-curated-specs/issues/188/requirements.md +0 -78
- data/ai-curated-specs/issues/188/tasks.md +0 -203
- data/ai-curated-specs/issues/188/upstream-contributions.md +0 -592
- data/ai-curated-specs/issues/50/design.md +0 -235
- data/ai-curated-specs/issues/50/requirements.md +0 -49
- data/ai-curated-specs/issues/50/tasks.md +0 -232
- data/bin/_dynamodblocal +0 -4
- data/bin/bundle_update.sh +0 -7
- data/bin/deploy_on_heroku.sh +0 -16
- data/bin/install_dynamodblocal.sh +0 -5
- data/bin/start_dynamodblocal.sh +0 -47
- data/bin/stop_dynamodblocal.sh +0 -34
- data/gemfiles/Gemfile.rails-5.0 +0 -25
- data/gemfiles/Gemfile.rails-5.1 +0 -25
- data/gemfiles/Gemfile.rails-5.2 +0 -24
- data/gemfiles/Gemfile.rails-6.0 +0 -23
- data/gemfiles/Gemfile.rails-6.1 +0 -22
- data/gemfiles/Gemfile.rails-7.0 +0 -25
- data/gemfiles/Gemfile.rails-7.1 +0 -23
- data/gemfiles/Gemfile.rails-7.2 +0 -23
- data/gemfiles/Gemfile.rails-8.0 +0 -24
- data/package.json +0 -8
- data/spec/channels/notification_api_channel_shared_examples.rb +0 -59
- data/spec/channels/notification_api_channel_spec.rb +0 -49
- data/spec/channels/notification_api_with_devise_channel_spec.rb +0 -76
- data/spec/channels/notification_channel_shared_examples.rb +0 -59
- data/spec/channels/notification_channel_spec.rb +0 -48
- data/spec/channels/notification_with_devise_channel_spec.rb +0 -97
- data/spec/concerns/apis/notification_api_spec.rb +0 -1627
- data/spec/concerns/apis/subscription_api_spec.rb +0 -474
- data/spec/concerns/common_spec.rb +0 -213
- data/spec/concerns/models/group_spec.rb +0 -61
- data/spec/concerns/models/notifiable_spec.rb +0 -782
- data/spec/concerns/models/notifier_spec.rb +0 -71
- data/spec/concerns/models/subscriber_spec.rb +0 -800
- data/spec/concerns/models/target_spec.rb +0 -1285
- data/spec/concerns/renderable_spec.rb +0 -129
- data/spec/config_spec.rb +0 -85
- data/spec/controllers/common_controller_spec.rb +0 -25
- data/spec/controllers/controller_spec_utility.rb +0 -100
- data/spec/controllers/dummy_common_controller.rb +0 -5
- data/spec/controllers/notifications_api_controller_shared_examples.rb +0 -619
- data/spec/controllers/notifications_api_controller_spec.rb +0 -19
- data/spec/controllers/notifications_api_with_devise_controller_spec.rb +0 -60
- data/spec/controllers/notifications_controller_shared_examples.rb +0 -743
- data/spec/controllers/notifications_controller_spec.rb +0 -11
- data/spec/controllers/notifications_with_devise_controller_spec.rb +0 -97
- data/spec/controllers/subscriptions_api_controller_shared_examples.rb +0 -750
- data/spec/controllers/subscriptions_api_controller_spec.rb +0 -19
- data/spec/controllers/subscriptions_api_with_devise_controller_spec.rb +0 -60
- data/spec/controllers/subscriptions_controller_shared_examples.rb +0 -946
- data/spec/controllers/subscriptions_controller_spec.rb +0 -11
- data/spec/controllers/subscriptions_with_devise_controller_spec.rb +0 -97
- data/spec/factories/admins.rb +0 -5
- data/spec/factories/articles.rb +0 -5
- data/spec/factories/comments.rb +0 -6
- data/spec/factories/dummy/dummy_group.rb +0 -4
- data/spec/factories/dummy/dummy_notifiable.rb +0 -4
- data/spec/factories/dummy/dummy_notifier.rb +0 -4
- data/spec/factories/dummy/dummy_subscriber.rb +0 -4
- data/spec/factories/dummy/dummy_target.rb +0 -4
- data/spec/factories/notifications.rb +0 -7
- data/spec/factories/subscriptions.rb +0 -8
- data/spec/factories/users.rb +0 -11
- data/spec/generators/controllers_generator_spec.rb +0 -85
- data/spec/generators/install_generator_spec.rb +0 -43
- data/spec/generators/migration/migration_generator_spec.rb +0 -80
- data/spec/generators/models_generator_spec.rb +0 -96
- data/spec/generators/views_generator_spec.rb +0 -195
- data/spec/helpers/polymorphic_helpers_spec.rb +0 -89
- data/spec/helpers/view_helpers_spec.rb +0 -547
- data/spec/jobs/notification_resilience_job_spec.rb +0 -167
- data/spec/jobs/notify_all_job_spec.rb +0 -23
- data/spec/jobs/notify_job_spec.rb +0 -23
- data/spec/jobs/notify_to_job_spec.rb +0 -23
- data/spec/mailers/mailer_spec.rb +0 -214
- data/spec/mailers/notification_resilience_spec.rb +0 -263
- data/spec/models/dummy/dummy_group_spec.rb +0 -10
- data/spec/models/dummy/dummy_notifiable_spec.rb +0 -10
- data/spec/models/dummy/dummy_notifier_spec.rb +0 -10
- data/spec/models/dummy/dummy_subscriber_spec.rb +0 -8
- data/spec/models/dummy/dummy_target_spec.rb +0 -10
- data/spec/models/notification_spec.rb +0 -472
- data/spec/models/subscription_spec.rb +0 -215
- data/spec/optional_targets/action_cable_api_channel_spec.rb +0 -34
- data/spec/optional_targets/action_cable_channel_spec.rb +0 -41
- data/spec/optional_targets/amazon_sns_spec.rb +0 -47
- data/spec/optional_targets/base_spec.rb +0 -45
- data/spec/optional_targets/slack_spec.rb +0 -44
- data/spec/orm/dynamoid_spec.rb +0 -115
- data/spec/rails_app/Rakefile +0 -15
- data/spec/rails_app/app/assets/config/manifest.js +0 -3
- data/spec/rails_app/app/assets/images/.keep +0 -0
- data/spec/rails_app/app/assets/javascripts/application.js +0 -3
- data/spec/rails_app/app/assets/javascripts/cable.js +0 -12
- data/spec/rails_app/app/assets/stylesheets/application.css +0 -15
- data/spec/rails_app/app/assets/stylesheets/reset.css +0 -85
- data/spec/rails_app/app/assets/stylesheets/style.css +0 -244
- data/spec/rails_app/app/controllers/admins_controller.rb +0 -21
- data/spec/rails_app/app/controllers/application_controller.rb +0 -5
- data/spec/rails_app/app/controllers/articles_controller.rb +0 -67
- data/spec/rails_app/app/controllers/comments_controller.rb +0 -36
- data/spec/rails_app/app/controllers/concerns/.keep +0 -0
- data/spec/rails_app/app/controllers/spa_controller.rb +0 -7
- data/spec/rails_app/app/controllers/users/notifications_controller.rb +0 -2
- data/spec/rails_app/app/controllers/users/notifications_with_devise_controller.rb +0 -2
- data/spec/rails_app/app/controllers/users/subscriptions_controller.rb +0 -2
- data/spec/rails_app/app/controllers/users/subscriptions_with_devise_controller.rb +0 -2
- data/spec/rails_app/app/controllers/users_controller.rb +0 -26
- data/spec/rails_app/app/helpers/application_helper.rb +0 -2
- data/spec/rails_app/app/helpers/devise_helper.rb +0 -2
- data/spec/rails_app/app/javascript/App.vue +0 -40
- data/spec/rails_app/app/javascript/components/DeviseTokenAuth.vue +0 -82
- data/spec/rails_app/app/javascript/components/Top.vue +0 -98
- data/spec/rails_app/app/javascript/components/notifications/Index.vue +0 -200
- data/spec/rails_app/app/javascript/components/notifications/Notification.vue +0 -133
- data/spec/rails_app/app/javascript/components/notifications/NotificationContent.vue +0 -122
- data/spec/rails_app/app/javascript/components/subscriptions/Index.vue +0 -279
- data/spec/rails_app/app/javascript/components/subscriptions/NewSubscription.vue +0 -112
- data/spec/rails_app/app/javascript/components/subscriptions/NotificationKey.vue +0 -141
- data/spec/rails_app/app/javascript/components/subscriptions/Subscription.vue +0 -226
- data/spec/rails_app/app/javascript/config/development.js +0 -5
- data/spec/rails_app/app/javascript/config/environment.js +0 -7
- data/spec/rails_app/app/javascript/config/production.js +0 -5
- data/spec/rails_app/app/javascript/config/test.js +0 -5
- data/spec/rails_app/app/javascript/packs/application.js +0 -18
- data/spec/rails_app/app/javascript/packs/spa.js +0 -14
- data/spec/rails_app/app/javascript/router/index.js +0 -73
- data/spec/rails_app/app/javascript/store/index.js +0 -37
- data/spec/rails_app/app/mailers/.keep +0 -0
- data/spec/rails_app/app/mailers/custom_notification_mailer.rb +0 -5
- data/spec/rails_app/app/models/admin.rb +0 -35
- data/spec/rails_app/app/models/article.rb +0 -54
- data/spec/rails_app/app/models/comment.rb +0 -81
- data/spec/rails_app/app/models/dummy/dummy_base.rb +0 -11
- data/spec/rails_app/app/models/dummy/dummy_group.rb +0 -23
- data/spec/rails_app/app/models/dummy/dummy_notifiable.rb +0 -15
- data/spec/rails_app/app/models/dummy/dummy_notifiable_target.rb +0 -27
- data/spec/rails_app/app/models/dummy/dummy_notifier.rb +0 -15
- data/spec/rails_app/app/models/dummy/dummy_subscriber.rb +0 -14
- data/spec/rails_app/app/models/dummy/dummy_target.rb +0 -16
- data/spec/rails_app/app/models/user.rb +0 -73
- data/spec/rails_app/app/views/activity_notification/mailer/dummy_subscribers/test_key.text.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/notifications/default/article/_update.html.erb +0 -146
- data/spec/rails_app/app/views/activity_notification/notifications/default/custom/_path_test.html.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/notifications/default/custom/_test.html.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/notifications/users/_custom_index.html.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/notifications/users/custom/_test.html.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/notifications/users/overridden/custom/_test.html.erb +0 -1
- data/spec/rails_app/app/views/activity_notification/optional_targets/admins/amazon_sns/comment/_default.text.erb +0 -10
- data/spec/rails_app/app/views/articles/_form.html.erb +0 -24
- data/spec/rails_app/app/views/articles/edit.html.erb +0 -8
- data/spec/rails_app/app/views/articles/index.html.erb +0 -113
- data/spec/rails_app/app/views/articles/new.html.erb +0 -7
- data/spec/rails_app/app/views/articles/show.html.erb +0 -49
- data/spec/rails_app/app/views/layouts/_header.html.erb +0 -46
- data/spec/rails_app/app/views/layouts/application.html.erb +0 -15
- data/spec/rails_app/app/views/spa/index.html.erb +0 -2
- data/spec/rails_app/babel.config.js +0 -72
- data/spec/rails_app/bin/bundle +0 -3
- data/spec/rails_app/bin/rails +0 -4
- data/spec/rails_app/bin/rake +0 -4
- data/spec/rails_app/bin/setup +0 -29
- data/spec/rails_app/bin/webpack +0 -18
- data/spec/rails_app/bin/webpack-dev-server +0 -18
- data/spec/rails_app/config/application.rb +0 -54
- data/spec/rails_app/config/boot.rb +0 -5
- data/spec/rails_app/config/cable.yml +0 -8
- data/spec/rails_app/config/database.yml +0 -36
- data/spec/rails_app/config/dynamoid.rb +0 -13
- data/spec/rails_app/config/environment.rb +0 -26
- data/spec/rails_app/config/environments/development.rb +0 -60
- data/spec/rails_app/config/environments/production.rb +0 -85
- data/spec/rails_app/config/environments/test.rb +0 -53
- data/spec/rails_app/config/initializers/activity_notification.rb +0 -104
- data/spec/rails_app/config/initializers/assets.rb +0 -11
- data/spec/rails_app/config/initializers/backtrace_silencers.rb +0 -7
- data/spec/rails_app/config/initializers/cookies_serializer.rb +0 -3
- data/spec/rails_app/config/initializers/copy_it.aws.rb.template +0 -6
- data/spec/rails_app/config/initializers/devise.rb +0 -278
- data/spec/rails_app/config/initializers/devise_token_auth.rb +0 -55
- data/spec/rails_app/config/initializers/filter_parameter_logging.rb +0 -4
- data/spec/rails_app/config/initializers/inflections.rb +0 -16
- data/spec/rails_app/config/initializers/mime_types.rb +0 -4
- data/spec/rails_app/config/initializers/mysql.rb +0 -9
- data/spec/rails_app/config/initializers/session_store.rb +0 -3
- data/spec/rails_app/config/initializers/wrap_parameters.rb +0 -14
- data/spec/rails_app/config/initializers/zeitwerk.rb +0 -10
- data/spec/rails_app/config/locales/activity_notification.en.yml +0 -26
- data/spec/rails_app/config/locales/devise.en.yml +0 -62
- data/spec/rails_app/config/mongoid.yml +0 -13
- data/spec/rails_app/config/routes.rb +0 -50
- data/spec/rails_app/config/secrets.yml +0 -22
- data/spec/rails_app/config/webpack/development.js +0 -5
- data/spec/rails_app/config/webpack/environment.js +0 -7
- data/spec/rails_app/config/webpack/loaders/vue.js +0 -6
- data/spec/rails_app/config/webpack/production.js +0 -5
- data/spec/rails_app/config/webpack/test.js +0 -5
- data/spec/rails_app/config/webpacker.yml +0 -97
- data/spec/rails_app/config.ru +0 -4
- data/spec/rails_app/db/migrate/20160716000000_create_test_tables.rb +0 -42
- data/spec/rails_app/db/migrate/20181209000000_create_activity_notification_tables.rb +0 -33
- data/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb +0 -10
- data/spec/rails_app/db/schema.rb +0 -98
- data/spec/rails_app/db/seeds.rb +0 -95
- data/spec/rails_app/lib/custom_optional_targets/console_output.rb +0 -16
- data/spec/rails_app/lib/custom_optional_targets/raise_error.rb +0 -14
- data/spec/rails_app/lib/custom_optional_targets/wrong_target.rb +0 -13
- data/spec/rails_app/lib/mailer_previews/mailer_preview.rb +0 -29
- data/spec/rails_app/package.json +0 -23
- data/spec/rails_app/postcss.config.js +0 -12
- data/spec/rails_app/public/404.html +0 -67
- data/spec/rails_app/public/422.html +0 -67
- data/spec/rails_app/public/500.html +0 -66
- data/spec/rails_app/public/favicon.ico +0 -0
- data/spec/roles/acts_as_group_spec.rb +0 -30
- data/spec/roles/acts_as_notifiable_spec.rb +0 -432
- data/spec/roles/acts_as_notifier_spec.rb +0 -30
- data/spec/roles/acts_as_target_spec.rb +0 -36
- data/spec/spec_helper.rb +0 -56
- data/spec/version_spec.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ac9e17e63d801b74604936ea099825e1eedd68c5f743b35c01e7af9e73d90ab
|
|
4
|
+
data.tar.gz: 07db03884cf03f86db283a7b5cbd92f7629277ea5f4f5a910629924471ff62e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91e6206957623ce19971e581afa07fe3b464b02273a968c17bbf145da5836aecbd9b0919b05fae844f912a003a9eeb0d2d4a1df011889ba6bcfb7d752c632346
|
|
7
|
+
data.tar.gz: 0efe5572854a47c271f6b79b1e0ad649b0d491c1ba4b5370a09a78a0876b4f684bf841069fbaba26fff24e3735ae6b11eeb9bf92ce75f359776d59fd526519ce
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* Grouping notifications (grouping like *"Kevin and 7 other users posted comments to this article"*)
|
|
25
25
|
* Email notification
|
|
26
26
|
* Batch email notification (event driven or periodical email notification, daily or weekly etc)
|
|
27
|
+
* Cascading notifications (progressive notification escalation through multiple channels with time delays)
|
|
27
28
|
* Push notification with [Action Cable](https://guides.rubyonrails.org/action_cable_overview.html)
|
|
28
29
|
* Subscription management (subscribing and unsubscribing for each target and notification type)
|
|
29
30
|
* REST API backend and [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification)
|
|
@@ -104,6 +105,7 @@ You can see sample single page application using [Vue.js](https://vuejs.org) as
|
|
|
104
105
|
- [Batch email subject](/docs/Functions.md#batch-email-subject)
|
|
105
106
|
- [i18n for batch email](/docs/Functions.md#i18n-for-batch-email)
|
|
106
107
|
- [Grouping notifications](/docs/Functions.md#grouping-notifications)
|
|
108
|
+
- [Cascading notifications](/docs/Functions.md#cascading-notifications)
|
|
107
109
|
- [Subscription management](/docs/Functions.md#subscription-management)
|
|
108
110
|
- [Configuring subscriptions](/docs/Functions.md#configuring-subscriptions)
|
|
109
111
|
- [Managing subscriptions](/docs/Functions.md#managing-subscriptions)
|
|
@@ -325,9 +327,9 @@ See [Testing](/docs/Testing.md#Testing).
|
|
|
325
327
|
|
|
326
328
|
## Documentation
|
|
327
329
|
|
|
328
|
-
|
|
330
|
+
`docs/` contains documentation for users to read. These files are included in the distributed Gem. `ai-docs/` contains AI-generated and design documents. These files are not included in the distributed Gem.
|
|
329
331
|
|
|
330
|
-
RubyDoc.info does not support parsing methods in *included* and *class_methods* of *ActiveSupport::Concern* currently.
|
|
332
|
+
See [API Reference](http://www.rubydoc.info/github/simukappu/activity_notification/index) for more details. RubyDoc.info does not support parsing methods in *included* and *class_methods* of *ActiveSupport::Concern* currently.
|
|
331
333
|
To read complete documents, please generate YARD documents on your local environment:
|
|
332
334
|
```console
|
|
333
335
|
$ git pull https://github.com/simukappu/activity_notification.git
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
if defined?(ActiveJob)
|
|
2
|
+
# Job to handle cascading notifications with time delays and read status checking.
|
|
3
|
+
# This job enables sequential delivery of notifications through different channels
|
|
4
|
+
# based on whether previous notifications were read.
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# cascade_config = [
|
|
8
|
+
# { delay: 10.minutes, target: :slack },
|
|
9
|
+
# { delay: 10.minutes, target: :email }
|
|
10
|
+
# ]
|
|
11
|
+
# CascadingNotificationJob.perform_later(notification.id, cascade_config, 0)
|
|
12
|
+
class ActivityNotification::CascadingNotificationJob < ActivityNotification.config.parent_job.constantize
|
|
13
|
+
queue_as ActivityNotification.config.active_job_queue
|
|
14
|
+
|
|
15
|
+
# Performs a single step in the cascading notification chain.
|
|
16
|
+
# Checks if the notification is still unread, and if so, triggers the next optional target
|
|
17
|
+
# and schedules the next step in the cascade.
|
|
18
|
+
#
|
|
19
|
+
# @param [Integer] notification_id ID of the notification to check
|
|
20
|
+
# @param [Array<Hash>] cascade_config Array of cascade step configurations
|
|
21
|
+
# @option cascade_config [ActiveSupport::Duration] :delay Time to wait before checking and sending
|
|
22
|
+
# @option cascade_config [Symbol, String] :target Name of the optional target to trigger (e.g., :slack, :email)
|
|
23
|
+
# @option cascade_config [Hash] :options Optional parameters to pass to the optional target
|
|
24
|
+
# @param [Integer] step_index Current step index in the cascade chain (0-based)
|
|
25
|
+
# @return [Hash, nil] Result of triggering the optional target, or nil if notification was read or not found
|
|
26
|
+
def perform(notification_id, cascade_config, step_index = 0)
|
|
27
|
+
# Find the notification using ORM-appropriate method
|
|
28
|
+
# :nocov:
|
|
29
|
+
notification = case ActivityNotification.config.orm
|
|
30
|
+
when :dynamoid
|
|
31
|
+
ActivityNotification::Notification.find(notification_id, raise_error: false)
|
|
32
|
+
when :mongoid
|
|
33
|
+
begin
|
|
34
|
+
ActivityNotification::Notification.find(notification_id)
|
|
35
|
+
rescue Mongoid::Errors::DocumentNotFound
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
ActivityNotification::Notification.find_by(id: notification_id)
|
|
40
|
+
end
|
|
41
|
+
# :nocov:
|
|
42
|
+
|
|
43
|
+
# Return early if notification not found or has been opened (read)
|
|
44
|
+
return nil if notification.nil? || notification.opened?
|
|
45
|
+
|
|
46
|
+
# Get current step configuration
|
|
47
|
+
current_step = cascade_config[step_index]
|
|
48
|
+
return nil if current_step.nil?
|
|
49
|
+
|
|
50
|
+
# Extract step parameters
|
|
51
|
+
target_name = current_step[:target] || current_step['target']
|
|
52
|
+
target_options = current_step[:options] || current_step['options'] || {}
|
|
53
|
+
|
|
54
|
+
# Trigger the optional target for this step
|
|
55
|
+
result = trigger_optional_target(notification, target_name, target_options)
|
|
56
|
+
|
|
57
|
+
# Schedule next step if available and notification is still unread
|
|
58
|
+
next_step_index = step_index + 1
|
|
59
|
+
if next_step_index < cascade_config.length
|
|
60
|
+
next_step = cascade_config[next_step_index]
|
|
61
|
+
delay = next_step[:delay] || next_step['delay']
|
|
62
|
+
|
|
63
|
+
if delay.present?
|
|
64
|
+
# Schedule the next step with the specified delay
|
|
65
|
+
self.class.set(wait: delay).perform_later(
|
|
66
|
+
notification_id,
|
|
67
|
+
cascade_config,
|
|
68
|
+
next_step_index
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Triggers a specific optional target for the notification
|
|
79
|
+
# @param [Notification] notification The notification instance
|
|
80
|
+
# @param [Symbol, String] target_name Name of the optional target
|
|
81
|
+
# @param [Hash] options Options to pass to the optional target
|
|
82
|
+
# @return [Hash] Result of triggering the target
|
|
83
|
+
def trigger_optional_target(notification, target_name, options = {})
|
|
84
|
+
target_name_sym = target_name.to_sym
|
|
85
|
+
|
|
86
|
+
# Get all configured optional targets for this notification
|
|
87
|
+
optional_targets = notification.notifiable.optional_targets(
|
|
88
|
+
notification.target.to_resources_name,
|
|
89
|
+
notification.key
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Find the matching optional target
|
|
93
|
+
optional_target = optional_targets.find do |ot|
|
|
94
|
+
ot.to_optional_target_name == target_name_sym
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if optional_target.nil?
|
|
98
|
+
Rails.logger.warn("Optional target '#{target_name}' not found for notification #{notification.id}")
|
|
99
|
+
return { target_name_sym => :not_configured }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check subscription status
|
|
103
|
+
unless notification.optional_target_subscribed?(target_name_sym)
|
|
104
|
+
Rails.logger.info("Target not subscribed to optional target '#{target_name}' for notification #{notification.id}")
|
|
105
|
+
return { target_name_sym => :not_subscribed }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Trigger the optional target
|
|
109
|
+
begin
|
|
110
|
+
optional_target.notify(notification, options)
|
|
111
|
+
Rails.logger.info("Successfully triggered optional target '#{target_name}' for notification #{notification.id}")
|
|
112
|
+
{ target_name_sym => :success }
|
|
113
|
+
rescue => e
|
|
114
|
+
Rails.logger.error("Failed to trigger optional target '#{target_name}' for notification #{notification.id}: #{e.message}")
|
|
115
|
+
if ActivityNotification.config.rescue_optional_target_errors
|
|
116
|
+
{ target_name_sym => e }
|
|
117
|
+
else
|
|
118
|
+
raise e
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/docs/Functions.md
CHANGED
|
@@ -99,7 +99,7 @@ If you use i18n for email, you can configure email subject in your locale files.
|
|
|
99
99
|
|
|
100
100
|
#### Other header fields
|
|
101
101
|
|
|
102
|
-
Similarly to the [Email subject](#email-subject), the `From`, `Reply-To` and `Message-ID` headers are configurable per notifiable model. From and reply to will override the `config.mailer_sender` config setting.
|
|
102
|
+
Similarly to the [Email subject](#email-subject), the `From`, `Reply-To`, `CC` and `Message-ID` headers are configurable per notifiable model. From and reply to will override the `config.mailer_sender` config setting.
|
|
103
103
|
|
|
104
104
|
```ruby
|
|
105
105
|
class Comment < ActiveRecord::Base
|
|
@@ -120,12 +120,88 @@ class Comment < ActiveRecord::Base
|
|
|
120
120
|
"no-reply.article+comment-#{self.id}@example.com"
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
def overriding_notification_email_cc(target, key)
|
|
124
|
+
# CC the article author on comment notifications
|
|
125
|
+
if key == "comment.create"
|
|
126
|
+
article.user.email
|
|
127
|
+
else
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
123
132
|
def overriding_notification_email_message_id(target, key)
|
|
124
133
|
"https://www.example.com/article/#{article.id}@example.com/"
|
|
125
134
|
end
|
|
126
135
|
end
|
|
127
136
|
```
|
|
128
137
|
|
|
138
|
+
#### CC (Carbon Copy) configuration
|
|
139
|
+
|
|
140
|
+
*activity_notification* supports CC (Carbon Copy) email addresses at three levels with the following priority order:
|
|
141
|
+
|
|
142
|
+
1. **Notifiable model override** (highest priority) - using `overriding_notification_email_cc` method
|
|
143
|
+
2. **Target model method** - using `mailer_cc` method
|
|
144
|
+
3. **Global configuration** - using `config.mailer_cc` setting
|
|
145
|
+
|
|
146
|
+
##### Global CC configuration
|
|
147
|
+
|
|
148
|
+
You can configure global CC recipients in *activity_notification.rb* initializer as *String*, *Array*, or *Proc*:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# Single CC recipient for all notifications
|
|
152
|
+
config.mailer_cc = 'admin@example.com'
|
|
153
|
+
|
|
154
|
+
# Multiple CC recipients for all notifications
|
|
155
|
+
config.mailer_cc = ['admin@example.com', 'support@example.com']
|
|
156
|
+
|
|
157
|
+
# Dynamic CC based on notification key
|
|
158
|
+
config.mailer_cc = ->(key) {
|
|
159
|
+
if key.include?('urgent')
|
|
160
|
+
['urgent@example.com', 'manager@example.com']
|
|
161
|
+
else
|
|
162
|
+
'admin@example.com'
|
|
163
|
+
end
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
##### Target-level CC configuration
|
|
168
|
+
|
|
169
|
+
You can define `mailer_cc` method in your target model to set CC recipients for that specific target:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class User < ActiveRecord::Base
|
|
173
|
+
acts_as_target
|
|
174
|
+
belongs_to :team_lead, class_name: 'User'
|
|
175
|
+
|
|
176
|
+
# Return single or multiple CC addresses
|
|
177
|
+
def mailer_cc
|
|
178
|
+
team_lead.present? ? team_lead.email : 'admin@example.com'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
##### Notifiable-level CC override
|
|
184
|
+
|
|
185
|
+
For the most granular control, implement `overriding_notification_email_cc` in your notifiable model to set CC per notification type:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
class Article < ActiveRecord::Base
|
|
189
|
+
acts_as_notifiable :users,
|
|
190
|
+
targets: ->(article, key) { [article.user] }
|
|
191
|
+
|
|
192
|
+
def overriding_notification_email_cc(target, key)
|
|
193
|
+
case key
|
|
194
|
+
when 'article.published'
|
|
195
|
+
['editor@example.com', 'marketing@example.com']
|
|
196
|
+
when 'article.flagged'
|
|
197
|
+
'moderation@example.com'
|
|
198
|
+
else
|
|
199
|
+
nil # Falls back to target's mailer_cc or global config
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
129
205
|
#### i18n for email
|
|
130
206
|
|
|
131
207
|
The subject of notification email can be put in your locale *.yml* files as **mail_subject** field:
|
|
@@ -263,6 +339,126 @@ notification:
|
|
|
263
339
|
|
|
264
340
|
Then, you will see *"Kevin and 7 other users posted 10 comments to your article"*.
|
|
265
341
|
|
|
342
|
+
### Cascading notifications
|
|
343
|
+
|
|
344
|
+
*activity_notification* provides cascading notifications that enable progressive notification escalation through multiple channels with time delays. This ensures important notifications are not missed while avoiding unnecessary interruptions when users have already engaged with earlier notification channels.
|
|
345
|
+
|
|
346
|
+
#### How cascading notifications work
|
|
347
|
+
|
|
348
|
+
Cascading notifications automatically send notifications through different channels (Slack, Email, SMS, etc.) with configurable time delays, but only if the user hasn't already read the notification:
|
|
349
|
+
|
|
350
|
+
1. User gets an in-app notification
|
|
351
|
+
2. ⏱️ Wait 10 minutes → Still unread? Send Slack message
|
|
352
|
+
3. ⏱️ Wait 10 more minutes → Still unread? Send Email
|
|
353
|
+
4. ⏱️ Wait 30 more minutes → Still unread? Send SMS
|
|
354
|
+
|
|
355
|
+
If the user reads the notification at any point, the cascade stops automatically.
|
|
356
|
+
|
|
357
|
+
#### Basic usage
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
# Create a notification
|
|
361
|
+
notification = Notification.create!(
|
|
362
|
+
target: user,
|
|
363
|
+
notifiable: comment,
|
|
364
|
+
key: 'comment.reply'
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Setup cascade: Slack after 10 min, Email after another 10 min
|
|
368
|
+
cascade_config = [
|
|
369
|
+
{ delay: 10.minutes, target: :slack },
|
|
370
|
+
{ delay: 10.minutes, target: :email }
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
# Start the cascade
|
|
374
|
+
notification.cascade_notify(cascade_config)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Configuration options
|
|
378
|
+
|
|
379
|
+
Each step in the cascade requires:
|
|
380
|
+
|
|
381
|
+
| Parameter | Type | Required | Description |
|
|
382
|
+
|-----------|------|----------|-------------|
|
|
383
|
+
| `delay` | Duration | Yes | How long to wait (e.g., `10.minutes`, `1.hour`) |
|
|
384
|
+
| `target` | Symbol/String | Yes | Optional target name (`:slack`, `:email`, etc.) |
|
|
385
|
+
| `options` | Hash | No | Custom options to pass to the target |
|
|
386
|
+
|
|
387
|
+
#### Advanced usage
|
|
388
|
+
|
|
389
|
+
**Immediate first notification:**
|
|
390
|
+
```ruby
|
|
391
|
+
# Send Slack immediately, then email if still unread
|
|
392
|
+
cascade_config = [
|
|
393
|
+
{ delay: 5.minutes, target: :slack },
|
|
394
|
+
{ delay: 10.minutes, target: :email }
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
notification.cascade_notify(cascade_config, trigger_first_immediately: true)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**With custom options:**
|
|
401
|
+
```ruby
|
|
402
|
+
cascade_config = [
|
|
403
|
+
{
|
|
404
|
+
delay: 5.minutes,
|
|
405
|
+
target: :slack,
|
|
406
|
+
options: { channel: '#urgent' }
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
delay: 10.minutes,
|
|
410
|
+
target: :email
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
notification.cascade_notify(cascade_config)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Integration with notification creation:**
|
|
418
|
+
```ruby
|
|
419
|
+
# In your controller
|
|
420
|
+
comment = Comment.create!(comment_params)
|
|
421
|
+
|
|
422
|
+
# Create notifications
|
|
423
|
+
comment.notify(:users, key: 'comment.new')
|
|
424
|
+
|
|
425
|
+
# Add cascade to all created notifications
|
|
426
|
+
comment.notifications.each do |notification|
|
|
427
|
+
cascade_config = [
|
|
428
|
+
{ delay: 10.minutes, target: :slack },
|
|
429
|
+
{ delay: 30.minutes, target: :email }
|
|
430
|
+
]
|
|
431
|
+
notification.cascade_notify(cascade_config)
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### Prerequisites
|
|
436
|
+
|
|
437
|
+
Before using cascading notifications, ensure:
|
|
438
|
+
|
|
439
|
+
1. **Optional targets are configured** on your notifiable models
|
|
440
|
+
2. **ActiveJob is configured** (default in Rails)
|
|
441
|
+
3. **Job queue is running** (Sidekiq, Delayed Job, etc.)
|
|
442
|
+
|
|
443
|
+
#### Common patterns
|
|
444
|
+
|
|
445
|
+
**Urgent notifications (fast escalation):**
|
|
446
|
+
```ruby
|
|
447
|
+
URGENT_CASCADE = [
|
|
448
|
+
{ delay: 2.minutes, target: :slack },
|
|
449
|
+
{ delay: 5.minutes, target: :email },
|
|
450
|
+
{ delay: 10.minutes, target: :sms }
|
|
451
|
+
].freeze
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Normal notifications (gentle escalation):**
|
|
455
|
+
```ruby
|
|
456
|
+
NORMAL_CASCADE = [
|
|
457
|
+
{ delay: 30.minutes, target: :slack },
|
|
458
|
+
{ delay: 1.hour, target: :email }
|
|
459
|
+
].freeze
|
|
460
|
+
```
|
|
461
|
+
|
|
266
462
|
|
|
267
463
|
### Subscription management
|
|
268
464
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
module ActivityNotification
|
|
2
|
+
# Defines API for cascading notifications included in Notification model.
|
|
3
|
+
# Cascading notifications enable sequential delivery through different channels
|
|
4
|
+
# based on read status, with configurable time delays between each step.
|
|
5
|
+
module CascadingNotificationApi
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# Starts a cascading notification chain with the specified configuration.
|
|
9
|
+
# The chain will automatically check the read status before each step and
|
|
10
|
+
# only proceed if the notification remains unread.
|
|
11
|
+
#
|
|
12
|
+
# @example Simple cascade with Slack then email
|
|
13
|
+
# notification.cascade_notify([
|
|
14
|
+
# { delay: 10.minutes, target: :slack },
|
|
15
|
+
# { delay: 10.minutes, target: :email }
|
|
16
|
+
# ])
|
|
17
|
+
#
|
|
18
|
+
# @example Cascade with custom options for each target
|
|
19
|
+
# notification.cascade_notify([
|
|
20
|
+
# { delay: 5.minutes, target: :slack, options: { channel: '#alerts' } },
|
|
21
|
+
# { delay: 10.minutes, target: :amazon_sns, options: { subject: 'Urgent' } },
|
|
22
|
+
# { delay: 15.minutes, target: :email }
|
|
23
|
+
# ])
|
|
24
|
+
#
|
|
25
|
+
# @param [Array<Hash>] cascade_config Array of cascade step configurations
|
|
26
|
+
# @option cascade_config [ActiveSupport::Duration] :delay Required. Time to wait before this step
|
|
27
|
+
# @option cascade_config [Symbol, String] :target Required. Name of the optional target (e.g., :slack, :email)
|
|
28
|
+
# @option cascade_config [Hash] :options Optional. Parameters to pass to the optional target
|
|
29
|
+
# @param [Hash] options Additional options for cascade
|
|
30
|
+
# @option options [Boolean] :validate (true) Whether to validate the cascade configuration
|
|
31
|
+
# @option options [Boolean] :trigger_first_immediately (false) Whether to trigger the first target immediately without delay
|
|
32
|
+
# @return [Boolean] true if cascade was initiated successfully, false otherwise
|
|
33
|
+
# @raise [ArgumentError] if cascade_config is invalid and :validate is true
|
|
34
|
+
def cascade_notify(cascade_config, options = {})
|
|
35
|
+
validate = options.fetch(:validate, true)
|
|
36
|
+
trigger_first_immediately = options.fetch(:trigger_first_immediately, false)
|
|
37
|
+
|
|
38
|
+
# Validate configuration if requested
|
|
39
|
+
if validate
|
|
40
|
+
validation_result = validate_cascade_config(cascade_config)
|
|
41
|
+
unless validation_result[:valid]
|
|
42
|
+
raise ArgumentError, "Invalid cascade configuration: #{validation_result[:errors].join(', ')}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Return false if cascade config is empty
|
|
47
|
+
return false if cascade_config.blank?
|
|
48
|
+
|
|
49
|
+
# Return false if notification is already opened
|
|
50
|
+
return false if opened?
|
|
51
|
+
|
|
52
|
+
if defined?(ActiveJob) && defined?(ActivityNotification::CascadingNotificationJob) &&
|
|
53
|
+
ActivityNotification::CascadingNotificationJob.respond_to?(:perform_later)
|
|
54
|
+
if trigger_first_immediately && cascade_config.any?
|
|
55
|
+
# Trigger first target immediately
|
|
56
|
+
first_step = cascade_config.first
|
|
57
|
+
target_name = first_step[:target] || first_step['target']
|
|
58
|
+
target_options = first_step[:options] || first_step['options'] || {}
|
|
59
|
+
|
|
60
|
+
# Perform the first step synchronously
|
|
61
|
+
perform_cascade_step(target_name, target_options)
|
|
62
|
+
|
|
63
|
+
# Schedule remaining steps if any
|
|
64
|
+
if cascade_config.length > 1
|
|
65
|
+
remaining_config = cascade_config[1..-1]
|
|
66
|
+
first_delay = remaining_config.first[:delay] || remaining_config.first['delay']
|
|
67
|
+
|
|
68
|
+
if first_delay.present?
|
|
69
|
+
ActivityNotification::CascadingNotificationJob
|
|
70
|
+
.set(wait: first_delay)
|
|
71
|
+
.perform_later(id, remaining_config, 0)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
# Schedule first step with its configured delay
|
|
76
|
+
first_step = cascade_config.first
|
|
77
|
+
first_delay = first_step[:delay] || first_step['delay']
|
|
78
|
+
|
|
79
|
+
if first_delay.present?
|
|
80
|
+
ActivityNotification::CascadingNotificationJob
|
|
81
|
+
.set(wait: first_delay)
|
|
82
|
+
.perform_later(id, cascade_config, 0)
|
|
83
|
+
else
|
|
84
|
+
# If no delay specified for first step, trigger immediately
|
|
85
|
+
ActivityNotification::CascadingNotificationJob
|
|
86
|
+
.perform_later(id, cascade_config, 0)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
true
|
|
91
|
+
else
|
|
92
|
+
Rails.logger.error("ActiveJob or CascadingNotificationJob not available for cascading notifications")
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Validates a cascade configuration array
|
|
98
|
+
#
|
|
99
|
+
# @param [Array<Hash>] cascade_config The configuration to validate
|
|
100
|
+
# @return [Hash] Hash with :valid (Boolean) and :errors (Array<String>) keys
|
|
101
|
+
def validate_cascade_config(cascade_config)
|
|
102
|
+
errors = []
|
|
103
|
+
|
|
104
|
+
if cascade_config.nil?
|
|
105
|
+
errors << "cascade_config cannot be nil"
|
|
106
|
+
return { valid: false, errors: errors }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
unless cascade_config.is_a?(Array)
|
|
110
|
+
errors << "cascade_config must be an Array"
|
|
111
|
+
return { valid: false, errors: errors }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if cascade_config.empty?
|
|
115
|
+
errors << "cascade_config cannot be empty"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
cascade_config.each_with_index do |step, index|
|
|
119
|
+
unless step.is_a?(Hash)
|
|
120
|
+
errors << "Step #{index} must be a Hash"
|
|
121
|
+
next
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check for required target parameter
|
|
125
|
+
target = step[:target] || step['target']
|
|
126
|
+
if target.nil?
|
|
127
|
+
errors << "Step #{index} missing required :target parameter"
|
|
128
|
+
elsif !target.is_a?(Symbol) && !target.is_a?(String)
|
|
129
|
+
errors << "Step #{index} :target must be a Symbol or String"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check for delay parameter (only required for steps after the first if not using trigger_first_immediately)
|
|
133
|
+
delay = step[:delay] || step['delay']
|
|
134
|
+
if delay.nil?
|
|
135
|
+
errors << "Step #{index} missing :delay parameter"
|
|
136
|
+
elsif !delay.respond_to?(:from_now) && !delay.is_a?(Numeric)
|
|
137
|
+
errors << "Step #{index} :delay must be an ActiveSupport::Duration or Numeric (seconds)"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Check options if present
|
|
141
|
+
options = step[:options] || step['options']
|
|
142
|
+
if options.present? && !options.is_a?(Hash)
|
|
143
|
+
errors << "Step #{index} :options must be a Hash"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
{ valid: errors.empty?, errors: errors }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Checks if a cascading notification is currently in progress for this notification
|
|
151
|
+
# This is a helper method that checks if there are scheduled jobs for this notification
|
|
152
|
+
#
|
|
153
|
+
# @return [Boolean] true if cascade jobs are scheduled (this is a best-effort check)
|
|
154
|
+
def cascade_in_progress?
|
|
155
|
+
# This is a best-effort check that returns false by default
|
|
156
|
+
# In production, you might want to track this state differently
|
|
157
|
+
# (e.g., in Redis, database flag, or by querying the job queue)
|
|
158
|
+
false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Performs a single cascade step immediately (synchronously)
|
|
164
|
+
# @api private
|
|
165
|
+
# @param [Symbol, String] target_name Name of the optional target
|
|
166
|
+
# @param [Hash] options Options to pass to the optional target
|
|
167
|
+
# @return [Hash] Result of the operation
|
|
168
|
+
def perform_cascade_step(target_name, options = {})
|
|
169
|
+
target_name_sym = target_name.to_sym
|
|
170
|
+
|
|
171
|
+
# Get all configured optional targets for this notification
|
|
172
|
+
optional_targets = notifiable.optional_targets(
|
|
173
|
+
target.to_resources_name,
|
|
174
|
+
key
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Find the matching optional target
|
|
178
|
+
optional_target = optional_targets.find do |ot|
|
|
179
|
+
ot.to_optional_target_name == target_name_sym
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if optional_target.nil?
|
|
183
|
+
Rails.logger.warn("Optional target '#{target_name}' not found for notification #{id}")
|
|
184
|
+
return { target_name_sym => :not_configured }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check subscription status
|
|
188
|
+
unless optional_target_subscribed?(target_name_sym)
|
|
189
|
+
Rails.logger.info("Target not subscribed to optional target '#{target_name}' for notification #{id}")
|
|
190
|
+
return { target_name_sym => :not_subscribed }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Trigger the optional target
|
|
194
|
+
begin
|
|
195
|
+
optional_target.notify(self, options)
|
|
196
|
+
Rails.logger.info("Successfully triggered optional target '#{target_name}' for notification #{id}")
|
|
197
|
+
{ target_name_sym => :success }
|
|
198
|
+
rescue => e
|
|
199
|
+
Rails.logger.error("Failed to trigger optional target '#{target_name}' for notification #{id}: #{e.message}")
|
|
200
|
+
if ActivityNotification.config.rescue_optional_target_errors
|
|
201
|
+
{ target_name_sym => e }
|
|
202
|
+
else
|
|
203
|
+
raise e
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
require 'activity_notification/apis/cascading_notification_api'
|
|
2
|
+
|
|
1
3
|
module ActivityNotification
|
|
2
4
|
# Defines API for notification included in Notification model.
|
|
3
5
|
module NotificationApi
|
|
4
6
|
extend ActiveSupport::Concern
|
|
7
|
+
include CascadingNotificationApi
|
|
5
8
|
|
|
6
9
|
included do
|
|
7
10
|
# Defines store_notification as private clas method
|
|
@@ -82,6 +82,15 @@ module ActivityNotification
|
|
|
82
82
|
# @return [String] Email address as sender of notification email.
|
|
83
83
|
attr_accessor :mailer_sender
|
|
84
84
|
|
|
85
|
+
# @overload mailer_cc
|
|
86
|
+
# Returns carbon copy (CC) email address(es) for notification email
|
|
87
|
+
# @return [String, Array<String>, Proc] CC email address(es) for notification email.
|
|
88
|
+
# @overload mailer_cc=(value)
|
|
89
|
+
# Sets carbon copy (CC) email address(es) for notification email
|
|
90
|
+
# @param [String, Array<String>, Proc] mailer_cc The new mailer_cc
|
|
91
|
+
# @return [String, Array<String>, Proc] CC email address(es) for notification email.
|
|
92
|
+
attr_accessor :mailer_cc
|
|
93
|
+
|
|
85
94
|
# @overload mailer
|
|
86
95
|
# Returns mailer class for email notification
|
|
87
96
|
# @return [String] Mailer class for email notification.
|
|
@@ -236,6 +245,7 @@ module ActivityNotification
|
|
|
236
245
|
@subscribe_to_email_as_default = nil
|
|
237
246
|
@subscribe_to_optional_targets_as_default = nil
|
|
238
247
|
@mailer_sender = nil
|
|
248
|
+
@mailer_cc = nil
|
|
239
249
|
@mailer = 'ActivityNotification::Mailer'
|
|
240
250
|
@parent_mailer = 'ActionMailer::Base'
|
|
241
251
|
@parent_job = 'ActiveJob::Base'
|
|
@@ -74,6 +74,7 @@ module ActivityNotification
|
|
|
74
74
|
subject: :subject_for,
|
|
75
75
|
from: :mailer_from,
|
|
76
76
|
reply_to: :mailer_reply_to,
|
|
77
|
+
cc: :mailer_cc,
|
|
77
78
|
message_id: nil
|
|
78
79
|
}.each do |header_name, default_method|
|
|
79
80
|
overridding_method_name = "overriding_notification_email_#{header_name.to_s}"
|
|
@@ -81,7 +82,12 @@ module ActivityNotification
|
|
|
81
82
|
@notification.notifiable.send(overridding_method_name, @target, key).present?
|
|
82
83
|
@notification.notifiable.send(overridding_method_name, @target, key)
|
|
83
84
|
elsif default_method
|
|
84
|
-
|
|
85
|
+
# Special handling for methods that take target instead of key
|
|
86
|
+
if [:mailer_cc].include?(default_method)
|
|
87
|
+
send(default_method, @target)
|
|
88
|
+
else
|
|
89
|
+
send(default_method, key)
|
|
90
|
+
end
|
|
85
91
|
else
|
|
86
92
|
nil
|
|
87
93
|
end
|
|
@@ -99,6 +105,26 @@ module ActivityNotification
|
|
|
99
105
|
target.mailer_to
|
|
100
106
|
end
|
|
101
107
|
|
|
108
|
+
# Returns carbon copy (CC) email address(es).
|
|
109
|
+
#
|
|
110
|
+
# @param [Object] target Target instance to notify
|
|
111
|
+
# @return [String, Array<String>, nil] CC email address(es) or nil
|
|
112
|
+
def mailer_cc(target)
|
|
113
|
+
if target.respond_to?(:mailer_cc)
|
|
114
|
+
target.mailer_cc
|
|
115
|
+
elsif ActivityNotification.config.mailer_cc.present?
|
|
116
|
+
if ActivityNotification.config.mailer_cc.is_a?(Proc)
|
|
117
|
+
# Get the notification key from current context
|
|
118
|
+
key = @notification ? @notification.key : nil
|
|
119
|
+
ActivityNotification.config.mailer_cc.call(key)
|
|
120
|
+
else
|
|
121
|
+
ActivityNotification.config.mailer_cc
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
102
128
|
# Returns sender email address as 'reply_to'.
|
|
103
129
|
#
|
|
104
130
|
# @param [String] key Key of the notification or batch notification email
|