diff --git a/app/admin/practices.rb b/app/admin/practices.rb index b252991ba..28cdffd06 100644 --- a/app/admin/practices.rb +++ b/app/admin/practices.rb @@ -36,6 +36,19 @@ end end + collection_action :send_email_to_all_editors_form, method: :get do + render 'send_email_to_all_editors_form' + end + + collection_action :send_email_to_all_editors, method: :post do + subject = ActionController::Base.helpers.sanitize(params[:email][:subject]) + message = ActionController::Base.helpers.sanitize(params[:email][:message]) + + Practice.send_email_to_all_editors(subject, message, current_user) + + redirect_to admin_practices_path, notice: "Your batch email to Innovation editors has been sent." + end + collection_action :export_published_practices_with_queri_format, method: :get do practices = Practice.published.sort_a_to_z @@ -87,6 +100,9 @@ div do link_to 'Published Practices QUERI Download', export_published_practices_with_queri_format_admin_practices_path, class: 'admin-download-published-practices float-right display-block text-bold border-0 radius-md margin-bottom-105' end + div do + link_to "Send Email to All Editors", send_email_to_all_editors_form_admin_practices_path, class: 'admin-email-all-practice-editors float-right display-block text-bold border-0 radius-md margin-bottom-105' + end selectable_column unless params[:scope] == "get_practice_owner_emails" id_column unless params[:scope] == "get_practice_owner_emails" column 'Practice Name', :name diff --git a/app/assets/images/dm-footer-logo.png b/app/assets/images/dm-footer-logo.png new file mode 100644 index 000000000..0de640555 Binary files /dev/null and b/app/assets/images/dm-footer-logo.png differ diff --git a/app/assets/javascripts/batch_email.es6 b/app/assets/javascripts/batch_email.es6 new file mode 100644 index 000000000..4d859e748 --- /dev/null +++ b/app/assets/javascripts/batch_email.es6 @@ -0,0 +1,51 @@ +document.addEventListener('DOMContentLoaded', function() { + let textArea = document.getElementById('emailMessage'); + let editorInitialized = false; + + function initTinyMCE() { + if (editorInitialized) return; + tinymce.init({ + selector: 'textarea#emailMessage', + menubar: false, + plugins: ["link", "lists"], + toolbar: + 'undo redo | bold italic | link bullist numlist superscript subscript | removeformat', + link_title: false, + link_assume_external_targets: false, + paste_data_images: false, + images_upload_url: '', + automatic_uploads: false, + file_picker_types: '', + }); + editorInitialized = true; + } + + textArea.addEventListener('focus', initTinyMCE); + + document.getElementById('previewEmailButton').addEventListener('click', function(event) { + event.preventDefault(); + + if (editorInitialized) { + tinymce.remove('#emailMessage'); + editorInitialized = false; + } + + document.getElementById('previewSubject').innerHTML = "Subject: " + document.getElementById('emailSubject').value + document.getElementById('previewMessage').innerHTML = textArea.value; + + document.getElementById('previewModal').style.display = 'block'; + }); + + let previewModal = document.getElementById('previewModal'); + previewModal.querySelector('[data-close-modal]').addEventListener('click', function() { + previewModal.style.display = 'none'; + }); + + window.confirmSendEmail = function() { + let sendEmail = confirm("This email will be sent to all Innovation editors, are you sure?"); + if (sendEmail) { + document.getElementById('emailForm').submit(); + } + previewModal.style.display = 'none'; + }; +}); diff --git a/app/assets/stylesheets/active_admin.scss b/app/assets/stylesheets/active_admin.scss index aa120115a..784d95e2c 100644 --- a/app/assets/stylesheets/active_admin.scss +++ b/app/assets/stylesheets/active_admin.scss @@ -289,6 +289,16 @@ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 1.0em; } + + .admin-email-all-practice-editors { + text-decoration: none !important; + background-color: #5ea3d3; + padding: 10px 20px; + color: #ffffff !important; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.0em; + margin-right: 2px; + } } .admin_site_metrics { diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 69174485a..218e0ccc3 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -10,4 +10,17 @@ def send_set_password(options = {}) mail(to: @user.email, subject: subject) end + + def send_email_to_editor(mailer_args) + @subject = mailer_args[:subject] + @message = mailer_args[:message] + + user_info = mailer_args[:user_info] + @user_name = user_info[:user_name] || "Innovation Editor" + @user_email = user_info[:email] + + @practices = mailer_args[:practices] + + mail(to: user_info[:email], subject: @subject) + end end diff --git a/app/models/practice.rb b/app/models/practice.rb index 48123b336..11deb4d1d 100644 --- a/app/models/practice.rb +++ b/app/models/practice.rb @@ -471,4 +471,65 @@ def as_json(*) def self.ransackable_attributes(auth_object = nil) ["name", "support_network_email", "user_email"] end + + def self.send_email_to_all_editors(subject, message, current_user) + user_practices_data = collect_users_and_their_practices_info(current_user) + + mailer_args = { + subject: subject, + message: message, + } + + user_practices_data.each do |user_data| + mailer_args[:user_info] = user_data[:user_info] + mailer_args[:practices] = user_data[:practices] + + AdminMailer.send_email_to_editor( + mailer_args + ).deliver_now + end + end + + def self.collect_users_and_their_practices_info(current_user) + user_practices = {} + + includes(:user).each do |practice| + if practice.user.present? && practice.published? + user_practices[practice.user] ||= Set.new + user_practices[practice.user] << practice + end + end + + PracticeEditor.includes(:user, :practice).each do |editor| + if editor.practice.published? + user_practices[editor.user] ||= Set.new + user_practices[editor.user] << editor.practice + end + end + + host_options = Rails.application.config.action_mailer.default_url_options + + # disallow bulk emails from being sent from deployed STG and DEV apps, should only send the + # email to the current_user if listed as `user` or `practice_editor` of a practice + unless Rails.env.test? || Rails.env.development? || ENV['PROD_SERVERNAME'] == 'PROD' + user_practices = user_practices.select do |user_practice| + user_practice == current_user + end + end + + user_practices.map do |user, practices| + { + user_info: { + user_name: user.first_name, + email: user.email + }, + practices: practices.map do |practice| + { + practice_name: practice.name, + show_url: Rails.application.routes.url_helpers.practice_url(practice, host_options), + } + end + } + end + end end diff --git a/app/views/admin/practices/_email_preview_modal.html.erb b/app/views/admin/practices/_email_preview_modal.html.erb new file mode 100644 index 000000000..15bae9b40 --- /dev/null +++ b/app/views/admin/practices/_email_preview_modal.html.erb @@ -0,0 +1,25 @@ + +
Hello [User Name],
+ +Visit your Innovation(s) at the following link(s) and click "Edit" to begin editing. Please note that only one person can edit the innovation at a time.
+ ++ If you have any questions, issues, or feedback to share with us please send an email to <%= mail_to 'Marketplace@VA.gov ', 'Marketplace@VA.gov ' %> +
+ +Hello<%= ' ' + @user_name %>,
++ <%= @message.html_safe %> +
+Visit your Innovation(s) at the following link(s) and click "Edit" to begin editing. Please note that only one person can edit the innovation at a time.
++ If you have any questions, issues, or feedback to share with us please send an email to <%= mail_to 'Marketplace@VA.gov ', 'Marketplace@VA.gov ' %> +
+ + + diff --git a/config/environments/development.rb b/config/environments/development.rb index 28db9bf8f..49e1dae9b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -35,6 +35,7 @@ config.action_mailer.delivery_method = :letter_opener config.action_mailer.perform_deliveries = true + config.action_mailer.asset_host = ENV.fetch('HOSTNAME') # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/config/environments/production.rb b/config/environments/production.rb index 8eb13c585..6a1efb62a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -127,6 +127,7 @@ config.action_mailer.smtp_settings = mail_settings config.action_mailer.default_url_options = { host: ENV.fetch('HOSTNAME') } + config.action_mailer.asset_host = ENV.fetch('HOSTNAME') # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector diff --git a/spec/factories/practice_editors.rb b/spec/factories/practice_editors.rb new file mode 100644 index 000000000..500bd20aa --- /dev/null +++ b/spec/factories/practice_editors.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :practice_editor do + association :practice + association :user + + after(:build) do |practice_editor| + practice_editor.email = practice_editor.user.email + end + end +end diff --git a/spec/features/admin/email_all_editors_spec.rb b/spec/features/admin/email_all_editors_spec.rb new file mode 100644 index 000000000..f4ccfb72b --- /dev/null +++ b/spec/features/admin/email_all_editors_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe 'Admin email all editors button', type: :feature, js: true do + let(:admin) { create(:user, :admin) } + let!(:users) { create_list(:user, 3) } + let!(:practices) { create_list(:practice, 3, user: admin, published: true) } + + before do + ActionMailer::Base.deliveries.clear + + practices.each do |practice| + create(:practice_editor, user: users.first, practice: practice) + end + + users.each_with_index do |user, index| + create(:practice_editor, user: user, practice: practices[index]) + end + + login_as(admin, scope: :user, run_callbacks: false) + visit admin_practices_path + end + + it 'sends an email to all editors when the button is clicked' do + expect(page).to have_content('Published Practices QUERI Download') + + click_link 'Send Email to All Editors' + + fill_in 'emailSubject', with: 'Important Update' + find('#emailMessage').click + + + execute_script("tinyMCE.get('emailMessage').setContent('Please review the latest changes.')") + + click_button 'Preview Email' + + within('#previewModal') do + click_button 'Send Email' + page.driver.browser.switch_to.alert.accept + end + + expect(page).to have_current_path(admin_practices_path) + expect(page).to have_content('Your batch email to Innovation editors has been sent.') + + (users + [admin]).each do |user| + email = ActionMailer::Base.deliveries.select { |e| e.to.include?(user.email) }.first + expect(email).not_to be_nil + expect(email.subject).to eq 'Important Update' + + user_practices = user == admin ? practices : PracticeEditor.where(user: user).map(&:practice) + user_practices.each do |practice| + expect(email.body.encoded).to include practice_path(practice) + end + end + end +end diff --git a/spec/models/practice_spec.rb b/spec/models/practice_spec.rb index e5a6e889c..21032e19a 100644 --- a/spec/models/practice_spec.rb +++ b/spec/models/practice_spec.rb @@ -205,4 +205,65 @@ end end end + + describe '.send_email_to_all_editors' do + it 'sends an email to the editor with exact email and practice info' do + admin = create(:user, :admin) + practice = create(:practice, user: admin, published: true) + subject_text = 'Important Update' + message_text = 'Please review the latest changes.' + + allow(AdminMailer).to receive_message_chain(:send_email_to_editor, :deliver_now) + + Practice.send_email_to_all_editors(subject_text, message_text, admin) + + expected_args = { + subject: subject_text, + message: message_text, + user_info: { + email: admin.email, + user_name: admin.first_name + }, + practices: [{ + practice_name: practice.name, + show_url: Rails.application.routes.url_helpers.practice_url(practice, Rails.application.config.action_mailer.default_url_options) + }] + } + + expect(AdminMailer).to have_received(:send_email_to_editor).with(expected_args).once + end + end + + describe '.collect_users_and_their_practices_info' do + let(:user) { create(:user) } + let(:other_user) { create(:user) } + let(:admin) { create(:user) } + let!(:practices) { create_list(:practice, 3, user: user, published: true) } + + before do + create(:practice_editor, user: other_user, practice: practices.first) + + create(:practice_editor, user: admin, practice: practices.second) + end + + it 'groups practices by their associated users and editors' do + result = Practice.collect_users_and_their_practices_info(nil) + + user_result = result.find { |u| u[:user_info][:email] == user.email } + other_user_result = result.find { |u| u[:user_info][:email] == other_user.email } + admin_result = result.find { |u| u[:user_info][:email] == admin.email } + + # Verify the structure and content for the main user + expect(user_result[:user_info][:user_name]).to eq user.first_name + expect(user_result[:practices].length).to eq 3 + + # Verify the structure and content for other_user + expect(other_user_result[:user_info][:user_name]).to eq other_user.first_name + expect(other_user_result[:practices].length).to eq 1 + + # Verify the structure and content for admin + expect(admin_result[:user_info][:user_name]).to eq admin.first_name + expect(admin_result[:practices].length).to eq 1 + end + end end