diff --git a/.gitignore b/.gitignore index 37f9388f6..18298e21b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ spec/dummy/public/uploads/* /vendor docker-compose.override.yml -.env \ No newline at end of file +.env +mysql +node_modules \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index c2b9ad0c8..877169968 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,11 +18,11 @@ GIT PATH remote: . specs: - fae-rails (3.0.0) + fae-rails (3.1.0) acts_as_list (~> 0.9.11) browser (~> 2.5.3) carrierwave - devise (~> 4.0) + devise devise-two-factor jquery-rails (~> 4.3.1) jquery-ui-rails (~> 6.0.1) @@ -36,6 +36,7 @@ PATH sass (>= 3.4.0) sass-rails (>= 5.0.7) simple_form (<= 5.1) + slack-notifier slim uglifier @@ -148,13 +149,12 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - carrierwave (2.2.2) - activemodel (>= 5.0.0) - activesupport (>= 5.0.0) + carrierwave (3.0.7) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) addressable (~> 2.6) image_processing (~> 1.1) marcel (~> 1.0.0) - mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) childprocess (4.1.0) chunky_png (1.4.0) @@ -175,7 +175,7 @@ GEM msgpack debase-ruby_core_source (0.10.16) debug_inspector (1.1.0) - devise (4.9.3) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -191,7 +191,7 @@ GEM erubi (1.12.0) eventmachine (1.2.7) excon (0.92.4) - execjs (2.8.1) + execjs (2.9.1) factory_bot (4.8.2) activesupport (>= 3.0.0) factory_bot_rails (4.8.2) @@ -283,7 +283,7 @@ GEM mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) - mini_magick (4.11.0) + mini_magick (4.12.0) mini_mime (1.1.2) minitest (5.21.2) msgpack (1.5.6) @@ -371,8 +371,8 @@ GEM actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.5) - rotp (6.2.0) - rqrcode (2.1.2) + rotp (6.3.0) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) @@ -398,7 +398,7 @@ GEM rspec-support (~> 3.10) rspec-support (3.11.0) ruby-prof (1.4.3) - ruby-vips (2.1.4) + ruby-vips (2.2.1) ffi (~> 1.12) rubyzip (2.3.2) sass (3.7.4) @@ -426,12 +426,13 @@ GEM simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) - slim (4.1.0) - temple (>= 0.7.6, < 0.9) - tilt (>= 2.0.6, < 2.1) - sprockets (4.0.3) + slack-notifier (2.4.0) + slim (5.2.1) + temple (~> 0.10.0) + tilt (>= 2.1.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -439,15 +440,15 @@ GEM sshkit (1.21.2) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - ssrf_filter (1.0.7) + ssrf_filter (1.1.2) strscan (3.0.1) - temple (0.8.2) + temple (0.10.3) thin (1.8.1) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (1.3.0) - tilt (2.0.10) + tilt (2.3.0) timeout (0.2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -487,7 +488,7 @@ DEPENDENCIES guard-rspec mysql2 pg - pry-nav + pry puma (~> 5.0) rails (~> 7.0.2) rails-controller-testing diff --git a/README.md b/README.md index 8a23b9718..9a78b9a42 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ https://www.faecms.com/documentation * [Form Field Label & Helper Text Manager](docs/features/form_manager.md) * [Netlify Deploy Monitor](docs/features/netlify.md) * [Multi-Factor Authentication via OTP](docs/features/mutli_factor_authentication.md) +* [Slack Notifications](docs/features/slack_notifications.md) ### Tutorials diff --git a/app/models/concerns/fae/base_model_concern.rb b/app/models/concerns/fae/base_model_concern.rb index 383e1f1a1..ca7464a0d 100644 --- a/app/models/concerns/fae/base_model_concern.rb +++ b/app/models/concerns/fae/base_model_concern.rb @@ -8,6 +8,31 @@ module BaseModelConcern included do include Fae::Trackable if Fae.track_changes include Fae::Sortable + after_create :notify_initiation + before_save :notify_changes + end + + def notify_changes + return unless notifiable_attributes.present? + notifiable_attributes.each do |field_name_symbol| + if self.send("#{field_name_symbol}_changed?") && self.send(field_name_symbol).present? + format_and_send_slack(field_name_symbol) + end + end + end + + def notify_initiation + return unless notifiable_attributes.present? + notifiable_attributes.each do |field_name_symbol| + if self.send(field_name_symbol).present? + format_and_send_slack(field_name_symbol) + end + end + end + + def notifiable_attributes + # override this method in your model + # array of attributes to notify if changed end def fae_display_field @@ -36,6 +61,17 @@ def fae_form_manager_model_id self.id end + def slack_message(field_name_symbol) + # override this method in your model + end + + def format_and_send_slack(field_name_symbol) + message = slack_message(field_name_symbol) + if message.present? + Fae::SlackNotification.new().send_slack(message: message) + end + end + module ClassMethods def for_fae_index order(order_method) diff --git a/app/services/fae/slack_notification.rb b/app/services/fae/slack_notification.rb new file mode 100644 index 000000000..55411d147 --- /dev/null +++ b/app/services/fae/slack_notification.rb @@ -0,0 +1,17 @@ +require 'slack-notifier' + +module Fae + class SlackNotification + + def send_slack(webhook: Fae.slack_webhook_url, message: nil) + if webhook.is_a?(String) + webhook = webhook.split(',') + end + webhook.each do |wh| + notifier = Slack::Notifier.new wh + notifier.ping message + end if webhook.present? + end + + end +end \ No newline at end of file diff --git a/docs/features/slack_notifications.md b/docs/features/slack_notifications.md new file mode 100644 index 000000000..a360963be --- /dev/null +++ b/docs/features/slack_notifications.md @@ -0,0 +1,48 @@ +# Slack Notifications + +Built around the `slack-notifier` gem, FAE now offers the ability to send a message to a Slack channel when an object is created or saved. + +Add the Slack webhook URL(s) to your `fae.rb` file. It can be a single value, or comma separated for multiple: + +``` +# fae.rb +config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] +``` + +FAE's `base_model_concern.rb` has two new callbacks: + +```ruby +after_create :notify_initiation +before_save :notify_changes +``` + +Models created with FAE's scaffold generators include this concern by default so you shouldn't need to take any action here. + +However, there are a couple of new instance methods to drive this in your models: + +```ruby +def notifiable_attributes + # array of attributes to notify if changed e.g.: + [:slug, :on_prod] +end +``` + + +```ruby +def slack_message(field_name_symbol) + case field_name_symbol + when :on_prod + status = self.on_prod? ? 'live' : 'not live' + msg = '' + msg += "#{Rails.application.class.module_parent_name} - " + msg += "[#{name}](#{Rails.application.routes.url_helpers.edit_admin_wine_url(self)}) " + msg += "(#{self.class.name.constantize}) is #{status} " + msg += "#{field_name_symbol.to_s.gsub('_',' ')}" + return msg + when :slug + # Different message for slug changes. + end +end +``` + +As illustrated, you can customize the messages as you wish. \ No newline at end of file diff --git a/fae.gemspec b/fae.gemspec index 2a8d7e785..32b728cdb 100644 --- a/fae.gemspec +++ b/fae.gemspec @@ -59,6 +59,7 @@ Gem::Specification.new do |s| s.add_dependency 'slim' s.add_dependency 'devise-two-factor' s.add_dependency 'rqrcode' + s.add_dependency 'slack-notifier' s.add_development_dependency 'appraisal' s.add_development_dependency 'better_errors' diff --git a/lib/fae/options.rb b/lib/fae/options.rb index b1dacd252..b471fbc59 100644 --- a/lib/fae/options.rb +++ b/lib/fae/options.rb @@ -4,7 +4,7 @@ module Fae # configurable defaults - mattr_accessor :devise_secret_key, :devise_mailer_sender, :dashboard_exclusions, :max_image_upload_size, :max_file_upload_size, :languages, :recreate_versions, :validation_helpers, :track_changes, :tracker_history_length, :slug_separator, :disabled_environments, :per_page, :use_cache, :use_form_manager, :netlify + mattr_accessor :devise_secret_key, :devise_mailer_sender, :dashboard_exclusions, :max_image_upload_size, :max_file_upload_size, :languages, :recreate_versions, :validation_helpers, :track_changes, :tracker_history_length, :slug_separator, :disabled_environments, :per_page, :use_cache, :use_form_manager, :netlify, :slack_webhook_url self.devise_secret_key = '' self.devise_mailer_sender = 'change-me@example.com' @@ -22,6 +22,7 @@ module Fae self.use_cache = false self.use_form_manager = false self.netlify = {} + self.slack_webhook_url = '' # this function maps the vars from your app into your engine def self.setup(&block) diff --git a/lib/generators/fae/templates/initializers/fae.rb b/lib/generators/fae/templates/initializers/fae.rb index 931294e58..aa11ce53c 100644 --- a/lib/generators/fae/templates/initializers/fae.rb +++ b/lib/generators/fae/templates/initializers/fae.rb @@ -75,4 +75,8 @@ # site_id: 'site-id-in-netlify', # api_base: 'https://api.netlify.com/api/v1/' # } + + ## slack_webhook_url + # Environment variable is recommended for any sensitive Slack configuration details. + config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] end \ No newline at end of file diff --git a/spec/dummy/app/models/wine.rb b/spec/dummy/app/models/wine.rb index 60b61f8aa..d3a0ec336 100644 --- a/spec/dummy/app/models/wine.rb +++ b/spec/dummy/app/models/wine.rb @@ -32,4 +32,17 @@ def self.for_fae_index order(:position) end + def notifiable_attributes + [:on_stage, :on_prod, :description_en] + end + + def slack_message(field_name_symbol) + msg = '' + msg += "#{Rails.application.class.module_parent_name} - " + msg += "[#{name}](#{Rails.application.routes.url_helpers.edit_admin_wine_url(self)}) " + msg += "(#{self.class.name.constantize}) is live " + msg += "#{field_name_symbol.to_s.gsub('_',' ')}" + msg + end + end diff --git a/spec/dummy/config/environment.rb b/spec/dummy/config/environment.rb index 10718a48b..1cdcb2cf0 100644 --- a/spec/dummy/config/environment.rb +++ b/spec/dummy/config/environment.rb @@ -4,4 +4,7 @@ # Initialize the Rails application. Rails.application.initialize! -ActiveRecord::Migrator.migrations_paths = '../spec/dummy/db/migrate' \ No newline at end of file +ActiveRecord::Migrator.migrations_paths = '../spec/dummy/db/migrate' + +# Set the default host and port to be the same as Action Mailer. +Rails.application.default_url_options = Rails.application.config.action_mailer.default_url_options \ No newline at end of file diff --git a/spec/dummy/config/initializers/fae.rb b/spec/dummy/config/initializers/fae.rb index b3d60657e..c698b917c 100644 --- a/spec/dummy/config/initializers/fae.rb +++ b/spec/dummy/config/initializers/fae.rb @@ -30,4 +30,6 @@ api_base: 'https://api.netlify.com/api/v1/' } end + + config.slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] end diff --git a/spec/models/fae/base_spec.rb b/spec/models/fae/base_spec.rb index ea4c99ee5..3ec6b7d9a 100644 --- a/spec/models/fae/base_spec.rb +++ b/spec/models/fae/base_spec.rb @@ -82,4 +82,27 @@ end end + describe '#format_and_send_slack' do + it 'should send a slack notification if message is present' do + wine = FactoryBot.create(:wine) + field_name_symbol = :on_prod + + test_message = "Dummy - [asdf](http://localhost/admin/wines/#{wine.id}/edit) (Wine) is live on prod" + allow(wine).to receive(:slack_message).with(field_name_symbol).and_return(test_message) + expect(Fae::SlackNotification).to receive(:new).and_return(double(send_slack: true)) + + wine.format_and_send_slack(field_name_symbol) + end + + it 'should not send a slack notification if message is not present' do + wine = FactoryBot.create(:wine) + field_name_symbol = :name_en + + allow(wine).to receive(:slack_message).with(field_name_symbol).and_return(nil) + expect(Fae::SlackNotification).not_to receive(:new) + + wine.format_and_send_slack(field_name_symbol) + end + end + end