diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb
index 422484a733..62e164c872 100644
--- a/app/controllers/imports_controller.rb
+++ b/app/controllers/imports_controller.rb
@@ -1,4 +1,6 @@
class ImportsController < ApplicationController
+ require "csv"
+
include ActionView::Helpers::UrlHelper
after_action :verify_authorized
@@ -6,12 +8,14 @@ def index
authorize :import
@import_type = params.fetch(:import_type, "volunteer")
@import_error = session[:import_error]
+ @sms_opt_in_warning = session[:sms_opt_in_warning]
session[:import_error] = nil
+ session[:sms_opt_in_warning] = nil
end
def create
authorize :import
- import = import_from_csv(params[:import_type], params[:file], current_user.casa_org_id)
+ import = import_from_csv(params[:import_type], params[:sms_opt_in], params[:file], current_user.casa_org_id)
message = import[:message]
# If there were failed imports
@@ -23,6 +27,8 @@ def create
if import[:type] == :error
session[:import_error] = message
+ elsif import[:type] == :sms_opt_in_warning
+ session[:sms_opt_in_warning] = import[:import_type]
# Only use flash for success messages. Otherwise may cause CookieOverflow
else
flash[:success] = message
@@ -60,11 +66,15 @@ def header_valid?(file_header, import_type)
file_header == header[import_type]
end
- def import_from_csv(import_type, file, org_id)
+ def import_from_csv(import_type, sms_opt_in, file, org_id)
validated_file = validate_file(file, import_type)
return validated_file unless validated_file.nil?
+ if requires_sms_opt_in(file, import_type, sms_opt_in)
+ return {type: :sms_opt_in_warning, import_type: import_type}
+ end
+
case import_type
when "volunteer"
VolunteerImporter.import_volunteers(file, org_id)
@@ -105,4 +115,23 @@ def validate_file(file, import_type)
{type: :error, message: message}
end
end
+
+ def requires_sms_opt_in(file, import_type, sms_opt_in)
+ if (import_type == "volunteer" || import_type == "supervisor") && import_contains_phone_numbers(file)
+ return sms_opt_in != "1"
+ end
+
+ false
+ end
+
+ def import_contains_phone_numbers(file)
+ CSV.foreach(file, headers: true, header_converters: :symbol) do |row|
+ phone_number = row[:phone_number]
+ if !phone_number.nil? && !phone_number.strip.empty?
+ return true
+ end
+ end
+
+ false
+ end
end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 4e58958d21..5bec593ceb 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -30,6 +30,7 @@ require('src/emancipations')
require('src/select')
require('src/dashboard')
require('src/sidebar')
+require('src/import')
require('src/readMore')
require('src/tooltip')
require('src/password_confirmation')
diff --git a/app/javascript/src/import.js b/app/javascript/src/import.js
new file mode 100644
index 0000000000..15dd38700f
--- /dev/null
+++ b/app/javascript/src/import.js
@@ -0,0 +1,71 @@
+/* global atob */
+/* global Blob */
+/* global FileReader */
+/* global localStorage */
+/* global File */
+/* global DataTransfer */
+
+function dataURItoBlob (dataURI) {
+ // convert base64 to raw binary data held in a string
+ const byteString = atob(dataURI.split(',')[1])
+
+ // separate out the mime component
+ const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
+
+ // write the bytes of the string to an ArrayBuffer
+ const arrayBuffer = new ArrayBuffer(byteString.length)
+ const _ia = new Uint8Array(arrayBuffer)
+ for (let i = 0; i < byteString.length; i++) {
+ _ia[i] = byteString.charCodeAt(i)
+ }
+
+ const dataView = new DataView(arrayBuffer)
+ const blob = new Blob([dataView], { type: mimeString })
+ return blob
+}
+
+function storeCSVFile (file, key) {
+ const reader = new FileReader()
+ reader.onload = (fileEvent) => {
+ localStorage[key] = JSON.stringify({
+ name: file.name,
+ data: fileEvent.target.result
+ })
+ }
+ reader.readAsDataURL(file)
+}
+
+function fetchCSVFile (key) {
+ const storedFileData = JSON.parse(localStorage[key])
+ const fileContent = dataURItoBlob(storedFileData.data)
+ const file = new File([fileContent], storedFileData.name, { type: 'text/csv' })
+ return file
+}
+
+function populateFileInput (inputId) {
+ const csvInput = document.getElementById(inputId)
+ if (csvInput.files.length === 0 && localStorage[inputId]) {
+ const file = fetchCSVFile(inputId)
+ const container = new DataTransfer()
+ container.items.add(file)
+ csvInput.files = container.files
+ }
+}
+
+$('document').ready(() => {
+ ['volunteer', 'supervisor'].forEach((importType) => {
+ const inputFileElementId = `${importType}-file`
+
+ document.getElementById(inputFileElementId).addEventListener('change', function (event) {
+ document.getElementById(`${importType}-import-button`).disabled = event.target.value === ''
+ const file = document.getElementById(inputFileElementId).files[0]
+ storeCSVFile(file, inputFileElementId)
+ })
+
+ if (document.getElementById('smsOptIn') == null) {
+ delete localStorage[inputFileElementId]
+ } else {
+ populateFileInput(inputFileElementId)
+ }
+ })
+})
diff --git a/app/lib/importers/supervisor_importer.rb b/app/lib/importers/supervisor_importer.rb
index 9d269e84e0..23cdefe436 100644
--- a/app/lib/importers/supervisor_importer.rb
+++ b/app/lib/importers/supervisor_importer.rb
@@ -18,6 +18,7 @@ def import_supervisors
end
supervisor_params[:phone_number] = supervisor_params.key?(:phone_number) ? "+#{supervisor_params[:phone_number]}" : ""
+ supervisor_params[:receive_sms_notifications] = !supervisor_params[:phone_number].empty?
supervisor = Supervisor.find_by(email: supervisor_params[:email])
volunteer_assignment_list = email_addresses_to_users(Volunteer, String(row[:supervisor_volunteers]))
@@ -32,15 +33,7 @@ def import_supervisors
supervisor = create_user_record(Supervisor, supervisor_params)
end
- volunteer_assignment_list.each do |volunteer|
- if volunteer.supervisor
- next if volunteer.supervisor == supervisor
-
- raise "Volunteer #{volunteer.email} already has a supervisor"
- else
- supervisor.volunteers << volunteer
- end
- end
+ assign_volunteers(supervisor, volunteer_assignment_list)
end
end
@@ -49,4 +42,14 @@ def update_supervisor(supervisor, supervisor_params, volunteer_assignment_list)
supervisor.update(supervisor_params)
end
end
+
+ def assign_volunteers(supervisor, volunteer_assignment_list)
+ volunteer_assignment_list.select { |v| v.supervisor != supervisor }.each do |volunteer|
+ if volunteer.supervisor
+ raise "Volunteer #{volunteer.email} already has a supervisor"
+ else
+ supervisor.volunteers << volunteer
+ end
+ end
+ end
end
diff --git a/app/lib/importers/volunteer_importer.rb b/app/lib/importers/volunteer_importer.rb
index 4485c3730c..c805498137 100644
--- a/app/lib/importers/volunteer_importer.rb
+++ b/app/lib/importers/volunteer_importer.rb
@@ -18,6 +18,7 @@ def import_volunteers
end
volunteer_params[:phone_number] = volunteer_params.key?(:phone_number) ? "+#{volunteer_params[:phone_number]}" : ""
+ volunteer_params[:receive_sms_notifications] = !volunteer_params[:phone_number].empty?
volunteer = Volunteer.find_by(email: volunteer_params[:email])
diff --git a/app/views/imports/_sms_opt_in_modal.html.erb b/app/views/imports/_sms_opt_in_modal.html.erb
new file mode 100644
index 0000000000..e46dc24d8d
--- /dev/null
+++ b/app/views/imports/_sms_opt_in_modal.html.erb
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ <%= t(".description") %>
+
+
+
+ <%= form.label :sms_opt_in_label, t(".label") %>
+ <%= form.check_box :sms_opt_in, { id: "sms-opt-in-checkbox" } %>
+
+
+
+
+
+
+
+
diff --git a/app/views/imports/_supervisors.html.erb b/app/views/imports/_supervisors.html.erb
index f84e3d86ed..c7a05852aa 100644
--- a/app/views/imports/_supervisors.html.erb
+++ b/app/views/imports/_supervisors.html.erb
@@ -13,7 +13,7 @@
2. <%= t(".upload_title") %>
- <%= form_with(url: imports_path, local: :true) do |f| %>
+ <%= form_with(url: imports_path, local: :true, id: "supervisor-import-form") do |f| %>
<%= f.hidden_field :import_type, value: "supervisor" %>
- <%= t(".instruction.click") %>
@@ -27,14 +27,9 @@
style: "margin: auto;" %>
+ <%= render "sms_opt_in_modal", { form: f } if @sms_opt_in_warning == "supervisor" %>
<%= button_tag id: "supervisor-import-button", class: "btn btn-md btn-success pull-right",
- disabled: true, data: {disable_with: " #{t(".disable_with")}"} do %>
+ disabled: true, data: { disable_with: " #{t(".disable_with")}"} do %>
<%= t(".button.import") %>
<% end %>
-
-
<% end %>
diff --git a/app/views/imports/_volunteers.html.erb b/app/views/imports/_volunteers.html.erb
index 5ecaf9973a..e50b9cedb5 100644
--- a/app/views/imports/_volunteers.html.erb
+++ b/app/views/imports/_volunteers.html.erb
@@ -13,8 +13,10 @@
2. <%= t(".upload_title") %>
- <%= form_with(url: imports_path, local: :true) do |f| %>
+ <%= form_with(url: imports_path, local: :true, id: "volunteer-import-form") do |f| %>
<%= f.hidden_field :import_type, value: "volunteer" %>
+ <%= f.hidden_field :sms_opt_in, value: false %>
+
- <%= t(".instruction.click") %>
- <%= t(".instruction.do_not") %> <%= t(".instruction.change_values") %>
@@ -22,14 +24,10 @@
<%= f.file_field :file, id: 'volunteer-file', accept: 'text/csv', class: 'form-control-file', style: "margin: auto;" %>
+
+ <%= render "sms_opt_in_modal", { form: f } if @sms_opt_in_warning == "volunteer" %>
<%= button_tag id: "volunteer-import-button", class: "btn btn-md btn-success pull-right",
- disabled: true, data: {disable_with: " #{t(".disable_with")}"} do %>
+ disabled: true, data: { disable_with: " #{t(".disable_with")}"} do %>
<%= t(".button.import") %>
<% end %>
-
-
<% end %>
diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml
index 87061ca82c..86f5fbc009 100644
--- a/config/locales/views.en.yml
+++ b/config/locales/views.en.yml
@@ -356,6 +356,11 @@ en:
do_not: Do not
import: Then click the "Import Supervisors CSV" button to import your supervisors.
upload_title: Upload your CSV file
+ sms_opt_in_modal:
+ title: SMS Opt In
+ description: Please check this box to verify that these mobile numbers have opted in to receive SMS notifications. (They will have the opportunity to opt out or update their preferences.)
+ label: Opt into SMS notifications
+ continue: Continue Import
volunteers:
button:
download: Download and reference example Volunteer CSV file
diff --git a/spec/fixtures/volunteers_without_phone_numbers.csv b/spec/fixtures/volunteers_without_phone_numbers.csv
index 495abe83cc..31e3958257 100644
--- a/spec/fixtures/volunteers_without_phone_numbers.csv
+++ b/spec/fixtures/volunteers_without_phone_numbers.csv
@@ -1,3 +1,3 @@
display_name,email,phone_number
-Volunteer One,volunteer1@example.net,11111111111
+Volunteer One,volunteer1@example.net,
Volunteer Two,volunteer2@example.net,
\ No newline at end of file
diff --git a/spec/lib/importers/supervisor_importer_spec.rb b/spec/lib/importers/supervisor_importer_spec.rb
index 313c139e58..e11afd4f3a 100644
--- a/spec/lib/importers/supervisor_importer_spec.rb
+++ b/spec/lib/importers/supervisor_importer_spec.rb
@@ -78,11 +78,12 @@
}.to change(existing_supervisor, :display_name).to("Supervisor Two")
end
- it "updates phone number to valid number" do
+ it "updates phone number to valid number and turns on sms notifications" do
expect {
supervisor_importer.import_supervisors
existing_supervisor.reload
}.to change(existing_supervisor, :phone_number).to("+11111111111")
+ .and change(existing_supervisor, :receive_sms_notifications).to(true)
end
end
@@ -101,13 +102,14 @@
context "when row doesn't have phone number" do
let(:supervisor_import_data_path) { Rails.root.join("spec", "fixtures", "supervisors_without_phone_numbers.csv") }
- let!(:existing_supervisor_with_number) { create(:supervisor, display_name: "#", email: "supervisor1@example.net", phone_number: "+11111111111") }
+ let!(:existing_supervisor_with_number) { create(:supervisor, display_name: "#", email: "supervisor1@example.net", phone_number: "+11111111111", receive_sms_notifications: true) }
- it "updates phone number to be deleted" do
+ it "updates phone number to be deleted and turns off sms notifications" do
expect {
supervisor_importer.import_supervisors
existing_supervisor_with_number.reload
}.to change(existing_supervisor_with_number, :phone_number).to("")
+ .and change(existing_supervisor_with_number, :receive_sms_notifications).to(false)
end
end
diff --git a/spec/lib/importers/volunteer_importer_spec.rb b/spec/lib/importers/volunteer_importer_spec.rb
index daa0b15df9..8e24d5ce6f 100644
--- a/spec/lib/importers/volunteer_importer_spec.rb
+++ b/spec/lib/importers/volunteer_importer_spec.rb
@@ -50,11 +50,12 @@
}.to change(existing_volunteer, :display_name).to("Volunteer One")
end
- it "updates phone number to valid number" do
+ it "updates phone number to valid number and turns sms notifications on" do
expect {
volunteer_importer.call
existing_volunteer.reload
}.to change(existing_volunteer, :phone_number).to("+11234567890")
+ .and change(existing_volunteer, :receive_sms_notifications).to(true)
end
end
@@ -73,13 +74,14 @@
context "when row doesn't have phone number" do
let(:import_file_path) { Rails.root.join("spec", "fixtures", "volunteers_without_phone_numbers.csv") }
- let!(:existing_volunteer_with_number) { create(:volunteer, display_name: "#", email: "volunteer2@example.net", phone_number: "+11111111111") }
+ let!(:existing_volunteer_with_number) { create(:volunteer, display_name: "#", email: "volunteer2@example.net", phone_number: "+11111111111", receive_sms_notifications: true) }
- it "updates phone number to be deleted" do
+ it "updates phone number to be deleted and turns sms notifications off" do
expect {
volunteer_importer.call
existing_volunteer_with_number.reload
}.to change(existing_volunteer_with_number, :phone_number).to("")
+ .and change(existing_volunteer_with_number, :receive_sms_notifications).to(false)
end
end
diff --git a/spec/requests/imports_spec.rb b/spec/requests/imports_spec.rb
index f1f61949a4..413c9eb235 100644
--- a/spec/requests/imports_spec.rb
+++ b/spec/requests/imports_spec.rb
@@ -30,7 +30,8 @@
post imports_url, params: {
import_type: "volunteer",
- file: upload_file(supervisor_file)
+ file: upload_file(supervisor_file),
+ sms_opt_in: "1"
}
expect(request.session[:import_error]).to include("Expected", VolunteerImporter::IMPORT_HEADER.join(", "))
@@ -42,7 +43,8 @@
post imports_url, params: {
import_type: "supervisor",
- file: upload_file(volunteer_file)
+ file: upload_file(volunteer_file),
+ sms_opt_in: "1"
}
expect(request.session[:import_error]).to include("Expected", SupervisorImporter::IMPORT_HEADER.join(", "))
@@ -54,7 +56,8 @@
post imports_url, params: {
import_type: "casa_case",
- file: upload_file(supervisor_file)
+ file: upload_file(supervisor_file),
+ sms_opt_in: "1"
}
expect(request.session[:import_error]).to include("Expected", CaseImporter::IMPORT_HEADER.join(", "))
@@ -70,7 +73,8 @@
post imports_url,
params: {
import_type: "volunteer",
- file: upload_file(volunteer_file)
+ file: upload_file(volunteer_file),
+ sms_opt_in: "1"
}
}.to change(Volunteer, :count).by(3)
@@ -89,7 +93,8 @@
post imports_url,
params: {
import_type: "supervisor",
- file: upload_file(supervisor_file)
+ file: upload_file(supervisor_file),
+ sms_opt_in: "1"
}
}.to change(Supervisor, :count).by(3)
@@ -112,7 +117,8 @@
post imports_url,
params: {
import_type: "supervisor",
- file: upload_file(supervisor_volunteers_file)
+ file: upload_file(supervisor_volunteers_file),
+ sms_opt_in: "1"
}
}.to change(Supervisor, :count).by(2)
@@ -131,7 +137,8 @@
post imports_url,
params: {
import_type: "casa_case",
- file: upload_file(case_file)
+ file: upload_file(case_file),
+ sms_opt_in: "1"
}
}.to change(CasaCase, :count).by(3)
diff --git a/spec/system/imports/index_spec.rb b/spec/system/imports/index_spec.rb
index 366a31558c..b340d219d3 100644
--- a/spec/system/imports/index_spec.rb
+++ b/spec/system/imports/index_spec.rb
@@ -1,7 +1,8 @@
require "rails_helper"
RSpec.describe "imports/index", type: :system do
- let(:volunteer) { build(:volunteer) }
+ let(:volunteer) { create(:volunteer) }
+ let(:admin) { create(:casa_admin) }
context "as a volunteer" do
before { sign_in volunteer }
@@ -12,4 +13,84 @@
expect(page).to have_selector(".alert", text: "Sorry, you are not authorized to perform this action.")
end
end
+
+ context "import volunteer csv with phone numbers", js: true do
+ let(:import_file_path) { Rails.root.join("spec", "fixtures", "volunteers.csv") }
+
+ it "shows sms opt in modal" do
+ sign_in admin
+ visit imports_path(:volunteer)
+
+ expect(page).to have_content("Import Volunteers")
+ expect(page).to have_button("volunteer-import-button", disabled: true)
+
+ attach_file "volunteer-file", import_file_path
+ click_button "volunteer-import-button"
+
+ expect(page).to have_text("SMS Opt In")
+ expect(page).to have_button("sms-opt-in-continue-button", disabled: true)
+
+ check "sms-opt-in-checkbox"
+ click_button "sms-opt-in-continue-button"
+
+ expect(page).to have_text("You successfully imported")
+ end
+ end
+
+ context "import volunteer csv without phone numbers", js: true do
+ let(:import_file_path) { Rails.root.join("spec", "fixtures", "volunteers_without_phone_numbers.csv") }
+
+ it "shows successful import" do
+ sign_in admin
+ visit imports_path(:volunteer)
+
+ expect(page).to have_content("Import Volunteers")
+
+ attach_file "volunteer-file", import_file_path
+ click_button "volunteer-import-button"
+
+ expect(page).to have_text("You successfully imported")
+ end
+ end
+
+ context "import supervisors csv with phone numbers", js: true do
+ let(:import_file_path) { Rails.root.join("spec", "fixtures", "supervisors.csv") }
+
+ it "shows sms opt in modal" do
+ sign_in admin
+ visit imports_path
+ click_on "supervisor-tab"
+
+ expect(page).to have_content("Import Supervisors")
+ expect(page).to have_button("supervisor-import-button", disabled: true)
+
+ attach_file "supervisor-file", import_file_path
+ click_button "supervisor-import-button"
+
+ expect(page).to have_text("SMS Opt In")
+ expect(page).to have_button("sms-opt-in-continue-button", disabled: true)
+
+ check "sms-opt-in-checkbox"
+ click_button "sms-opt-in-continue-button"
+
+ expect(page).to have_text("You successfully imported")
+ end
+ end
+
+ context "import supervisors csv without phone numbers", js: true do
+ let(:import_file_path) { Rails.root.join("spec", "fixtures", "supervisors_without_phone_numbers.csv") }
+
+ it "shows successful import" do
+ sign_in admin
+ visit imports_path
+ click_link "supervisor-tab"
+
+ expect(page).to have_content("Import Supervisors")
+
+ attach_file "supervisor-file", import_file_path
+ click_button "supervisor-import-button"
+
+ expect(page).to have_text("You successfully imported")
+ end
+ end
end