diff --git a/app/services/organizations/importers/google_csv_import_service.rb b/app/services/organizations/importers/google_csv_import_service.rb index d29723be9..cc167b58a 100644 --- a/app/services/organizations/importers/google_csv_import_service.rb +++ b/app/services/organizations/importers/google_csv_import_service.rb @@ -13,42 +13,64 @@ def initialize(file) end def call - CSV.foreach(@file.to_path, headers: true, skip_blanks: true).with_index(1) do |row, index| - # Using Google Form headers - email = row["Email"].downcase - csv_timestamp = Time.parse(row["Timestamp"]) - - person = Person.find_by(email:, organization: @organization) - previously_matched_form_submission = FormSubmission.where(person:, csv_timestamp:) - - if person.nil? - @no_match << [index, email] - elsif previously_matched_form_submission.present? - next - else - latest_form_submission = person.latest_form_submission - ActiveRecord::Base.transaction do - # This checks for the empty form submission that is added when a person is created - if latest_form_submission.csv_timestamp.nil? && latest_form_submission.form_answers.empty? - latest_form_submission.update!(csv_timestamp:) - create_form_answers(latest_form_submission, row) - else - # if the person submits a new/updated form, - # i.e. an additional row in the csv with the same email / different timestamp, - # a new form_submission will be created - create_form_answers(FormSubmission.create!(person:, csv_timestamp:), row) + catch(:halt_import) do + validate_file + + CSV.foreach(@file.to_path, headers: true, skip_blanks: true).with_index(1) do |row, index| + # Using Google Form headers + email = row[@email_header].downcase + csv_timestamp = Time.parse(row["Timestamp"]) + + person = Person.find_by(email:, organization: @organization) + previously_matched_form_submission = FormSubmission.where(person:, csv_timestamp:) + + if person.nil? + @no_match << [index, email] + elsif previously_matched_form_submission.present? + next + else + latest_form_submission = person.latest_form_submission + ActiveRecord::Base.transaction do + # This checks for the empty form submission that is added when a person is created + if latest_form_submission.csv_timestamp.nil? && latest_form_submission.form_answers.empty? + latest_form_submission.update!(csv_timestamp:) + create_form_answers(latest_form_submission, row) + else + # if the person submits a new/updated form, + # i.e. an additional row in the csv with the same email / different timestamp, + # a new form_submission will be created + create_form_answers(FormSubmission.create!(person:, csv_timestamp:), row) + end + @count += 1 end - @count += 1 end + rescue => e + @errors << [index, e] end - rescue => e - @errors << [index, e] end Status.new(@errors.empty?, @count, @no_match, @errors) end private + def validate_file + raise FileTypeError unless @file.content_type == "text/csv" + + first_row = CSV.foreach(@file.to_path).first + raise FileEmptyError if first_row.nil? + + raise TimestampColumnError unless first_row.include?("Timestamp") + + email_headers = ["Email", "email", "Email Address", "email address"] + email_headers.each do |e| + @email_header = e if first_row.include?(e) + end + raise EmailColumnError unless @email_header + rescue FileTypeError, FileEmptyError, TimestampColumnError, EmailColumnError => e + @errors << e + throw :halt_import + end + def create_form_answers(form_submission, row) row.each do |col| next if col[0] == "Email" || col[0] == "Timestamp" @@ -61,6 +83,30 @@ def create_form_answers(form_submission, row) ) end end + + class EmailColumnError < StandardError + def message + 'The column header "Email" was not found in the attached csv' + end + end + + class TimestampColumnError < StandardError + def message + 'The column header "Timestamp" was not found in the attached csv' + end + end + + class FileTypeError < StandardError + def message + "Invalid File Type: File type must be CSV" + end + end + + class FileEmptyError < StandardError + def message + "File is empty" + end + end end end end diff --git a/test/services/organizations/csv_import_service_test.rb b/test/services/organizations/csv_import_service_test.rb index 9794b0bae..0891f5cee 100644 --- a/test/services/organizations/csv_import_service_test.rb +++ b/test/services/organizations/csv_import_service_test.rb @@ -8,6 +8,8 @@ class CsvImportServiceTest < ActiveSupport::TestCase Current.organization = adopter.organization @file = Tempfile.new(["test", ".csv"]) + @file.stubs(:content_type).returns("text/csv") + headers = ["Timestamp", "First name", "Last name", "Email", "Address", "Phone number", *Faker::Lorem.questions] @data = [ @@ -110,5 +112,45 @@ class CsvImportServiceTest < ActiveSupport::TestCase refute import.success? assert_equal "mon out of range", import.errors[0][1].message end + + should "validate file type" do + file = Tempfile.new(["test", ".png"]) + file.stubs(:content_type).returns("image/png") + import = Organizations::Importers::GoogleCsvImportService.new(file).call + + assert_equal "Invalid File Type: File type must be CSV", import.errors.first.message + end + + should "validate empty file" do + file = Tempfile.new(["test", ".csv"]) + file.stubs(:content_type).returns("text/csv") + import = Organizations::Importers::GoogleCsvImportService.new(file).call + + assert_equal "File is empty", import.errors.first.message + end + + should "validate email header" do + file = Tempfile.new(["test", ".csv"]) + headers = ["Timestamp", "First name", "Last name", "Address", "Phone number", *Faker::Lorem.questions] + CSV.open(file.path, "wb") do |csv| + csv << headers + end + file.stubs(:content_type).returns("text/csv") + import = Organizations::Importers::GoogleCsvImportService.new(file).call + + assert_equal 'The column header "Email" was not found in the attached csv', import.errors.first.message + end + + should "validate Timestamp header" do + file = Tempfile.new(["test", ".csv"]) + headers = ["Email", "First name", "Last name", "Address", "Phone number", *Faker::Lorem.questions] + CSV.open(file.path, "wb") do |csv| + csv << headers + end + file.stubs(:content_type).returns("text/csv") + import = Organizations::Importers::GoogleCsvImportService.new(file).call + + assert_equal 'The column header "Timestamp" was not found in the attached csv', import.errors.first.message + end end end