Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c1b13f9
add sms opt in modal to volunteer import page
harsohailB Apr 17, 2022
eb53a09
add correct text on model and toggle continue button disability
harsohailB Apr 17, 2022
ae9bf5a
add functionality to submit import form from opt in modal
harsohailB Apr 17, 2022
387ab55
check for sms opt in required on server side
harsohailB Apr 17, 2022
9759ba4
add check for phone number values in import file
harsohailB Apr 17, 2022
77393e7
add sms opt in modal to supervisor import
harsohailB Apr 17, 2022
f980752
fix linting issues
harsohailB Apr 20, 2022
5b9a8e7
feat: temporarily store uploaded CSV in local storage while user opts in
harsohailB Apr 22, 2022
d8c2da5
delete local storage temp csv file upon successful import
harsohailB Apr 22, 2022
a7d5a90
improve checking of phone number existence in import csv
harsohailB Apr 22, 2022
f8bf146
delete local storage temp file when opt in message not present
harsohailB Apr 22, 2022
b1d4f8e
fix continue import button for supervisor import
harsohailB Apr 22, 2022
8b1a54c
toggle sms notifications when opt in checked
harsohailB Apr 22, 2022
ff2a804
fix linting issues
harsohailB Apr 22, 2022
ec6c974
add undefined variables to js global scope
harsohailB Apr 22, 2022
24a9f11
update import request and importer tests
harsohailB Apr 22, 2022
042b443
add system tests for sms opt in modal
harsohailB Apr 23, 2022
f2c66f8
reduce code complexity and duplication
harsohailB Apr 23, 2022
9366822
reduce supervisor import function to be the accepted 25 lines
harsohailB Apr 23, 2022
ec460bc
abstract out volunteer assignment in supervisor import to reduce func…
harsohailB Apr 23, 2022
c95c1ab
fix linting issues and code complexity
harsohailB Apr 23, 2022
d2c3491
remove next if statement to remove code complexity
harsohailB Apr 23, 2022
52ff24e
change sms opt in error to be named as warning
harsohailB Apr 24, 2022
42b32f1
Merge branch 'main' into feature/1790
harsohailB Apr 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
class ImportsController < ApplicationController
require "csv"

include ActionView::Helpers::UrlHelper
after_action :verify_authorized

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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
71 changes: 71 additions & 0 deletions app/javascript/src/import.js
Original file line number Diff line number Diff line change
@@ -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)
}
})
})
21 changes: 12 additions & 9 deletions app/lib/importers/supervisor_importer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
Expand All @@ -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

Expand All @@ -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
1 change: 1 addition & 0 deletions app/lib/importers/volunteer_importer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down
33 changes: 33 additions & 0 deletions app/views/imports/_sms_opt_in_modal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div class="modal show" id="smsOptIn" style="display: block;">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="smsOptIn">
<%= t(".title") %>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div>
<%= t(".description") %>
</div>
<br>
<span>
<%= form.label :sms_opt_in_label, t(".label") %>
<%= form.check_box :sms_opt_in, { id: "sms-opt-in-checkbox" } %>
<span>
</div>
<div class="modal-footer">
<%= form.submit t(".continue"), { id: "sms-opt-in-continue-button", class: "btn btn-primary", disabled: true } %>
</div>
</div>
</div>

<script type="text/javascript">
document.getElementById('sms-opt-in-checkbox').addEventListener('change', function (event) {
document.getElementById('sms-opt-in-continue-button').disabled = !event.target.checked;
})
</script>
</div>
11 changes: 3 additions & 8 deletions app/views/imports/_supervisors.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
2. <%= t(".upload_title") %>
<i class="fa fa-file-csv" aria-hidden="true"></i>
</h4>
<%= 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" %>
<ul>
<li><%= t(".instruction.click") %></li>
Expand All @@ -27,14 +27,9 @@
style: "margin: auto;" %>
</div>

<%= 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: "<i class='fa fa-spinner fa-spin'></i> #{t(".disable_with")}"} do %>
disabled: true, data: { disable_with: "<i class='fa fa-spinner fa-spin'></i> #{t(".disable_with")}"} do %>
<i class="fa fa-upload"></i> <%= t(".button.import") %>
<% end %>

<script type="text/javascript">
document.getElementById('supervisor-file').addEventListener('change', function (event) {
document.getElementById('supervisor-import-button').disabled = (event.target.value == '')
})
</script>
<% end %>
14 changes: 6 additions & 8 deletions app/views/imports/_volunteers.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,21 @@
2. <%= t(".upload_title") %>
<i class="fa fa-file-csv" aria-hidden="true"></i>
</h4>
<%= 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 %>

<ul>
<li><%= t(".instruction.click") %></li>
<li><strong><%= t(".instruction.do_not") %></strong> <%= t(".instruction.change_values") %></li>
<li><%= t(".instruction.import") %> <strong><%= t(".instruction.note") %></strong></li>
</ul>
<%= f.file_field :file, id: 'volunteer-file', accept: 'text/csv', class: 'form-control-file', style: "margin: auto;" %>
</div>

<%= 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: "<i class='fa fa-spinner fa-spin'></i> #{t(".disable_with")}"} do %>
disabled: true, data: { disable_with: "<i class='fa fa-spinner fa-spin'></i> #{t(".disable_with")}"} do %>
<i class="fa fa-upload"></i> <%= t(".button.import") %>
<% end %>

<script type="text/javascript">
document.getElementById('volunteer-file').addEventListener('change', function (event) {
document.getElementById('volunteer-import-button').disabled = (event.target.value == '')
})
</script>
<% end %>
5 changes: 5 additions & 0 deletions config/locales/views.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/volunteers_without_phone_numbers.csv
Original file line number Diff line number Diff line change
@@ -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,
8 changes: 5 additions & 3 deletions spec/lib/importers/supervisor_importer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
8 changes: 5 additions & 3 deletions spec/lib/importers/volunteer_importer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading