diff --git a/Gemfile b/Gemfile index 960b25729..d08ba4763 100644 --- a/Gemfile +++ b/Gemfile @@ -104,10 +104,10 @@ gem "ransack" gem "rails-controller-testing" # Use Action Policy for authorization framework -gem "action_policy", "~> 0.7.2" +gem "action_policy", "~> 0.7.3" # Use ViewComponent for our presenter pattern framework -gem "view_component", "~> 3.20" +gem "view_component", "~> 3.21" # Use dry-types for defining types gem "dry-types", "~> 1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 5b21fb911..dcbf0bcfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - action_policy (0.7.2) + action_policy (0.7.3) ruby-next-core (>= 1.0) actioncable (8.0.1) actionpack (= 8.0.1) @@ -146,7 +146,7 @@ GEM railties (>= 6.0.0) sass-embedded (~> 1.63) date (3.4.1) - debug (1.9.2) + debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) devise (4.9.4) @@ -277,7 +277,7 @@ GEM activesupport (>= 6.0.0) railties (>= 6.0.0) io-console (0.8.0) - irb (1.14.2) + irb (1.14.3) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.13.0) @@ -310,7 +310,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.3) + logger (1.6.4) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -439,7 +439,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.9.1) + rdoc (6.10.0) psych (>= 4.0.0) regexp_parser (2.9.3) reline (0.6.0) @@ -542,7 +542,7 @@ GEM unicode-emoji (4.0.4) uri (1.0.2) useragent (0.16.11) - view_component (3.20.0) + view_component (3.21.0) activesupport (>= 5.2.0, < 8.1) concurrent-ruby (~> 1.0) method_source (~> 1.0) @@ -570,7 +570,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - action_policy (~> 0.7.2) + action_policy (~> 0.7.3) active_link_to active_storage_validations acts_as_tenant @@ -625,7 +625,7 @@ DEPENDENCIES traceroute turbo-rails tzinfo-data - view_component (~> 3.20) + view_component (~> 3.21) web-console RUBY VERSION diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index 9e60c609b..9725ea9fa 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -4,6 +4,7 @@ class ContactsController < ApplicationController def new @contact = Contact.new + @contact.email = current_user.email if current_user.present? end def create diff --git a/app/controllers/feedback_controller.rb b/app/controllers/feedback_controller.rb index c03ecac77..749e75fd3 100644 --- a/app/controllers/feedback_controller.rb +++ b/app/controllers/feedback_controller.rb @@ -7,6 +7,7 @@ class FeedbackController < ApplicationController def new @feedback = Feedback.new + @feedback.email = current_user.email if current_user.present? end def create diff --git a/app/controllers/organizations/staff/external_form_upload_controller.rb b/app/controllers/organizations/staff/external_form_upload_controller.rb index 0e209ab92..6e759f42f 100644 --- a/app/controllers/organizations/staff/external_form_upload_controller.rb +++ b/app/controllers/organizations/staff/external_form_upload_controller.rb @@ -11,8 +11,15 @@ def index def create authorize! :external_form_upload, context: {organization: Current.organization} - import = Organizations::Importers::CsvImportService.new(params[:files]).call - render turbo_stream: turbo_stream.replace("results", partial: "organizations/staff/external_form_upload/upload_results", locals: {import: import}) + file = params[:files] + @blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: file.original_filename) + + CsvImportJob.perform_later(@blob.signed_id, current_user.id) + + flash.now[:notice] = t(".processing_file") + respond_to do |format| + format.turbo_stream + end end end end diff --git a/app/jobs/csv_import_job.rb b/app/jobs/csv_import_job.rb new file mode 100644 index 000000000..1a97e0128 --- /dev/null +++ b/app/jobs/csv_import_job.rb @@ -0,0 +1,11 @@ +class CsvImportJob < ApplicationJob + queue_as :default + + def perform(blob_signed_id, current_user_id) + blob = ActiveStorage::Blob.find_signed(blob_signed_id) + + Organizations::Importers::CsvImportService.new(blob, current_user_id).call + ensure + blob.purge_later + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index 091b5b7cd..05eb07764 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -2,7 +2,8 @@ class Contact include ActiveModel::Model attr_accessor :name, :email, :message - validates :name, :email, :message, presence: true + validates :name, :email, presence: true + validates :message, presence: true, length: {maximum: 500} # credit: https://medium.com/@limichelle21/building-and-debugging-a-contact-form-with-rails-mailgun-heroku-c0185b8bf419 end diff --git a/app/models/organization_account_request.rb b/app/models/organization_account_request.rb index 1d1eb659c..85b64b116 100644 --- a/app/models/organization_account_request.rb +++ b/app/models/organization_account_request.rb @@ -5,7 +5,7 @@ class OrganizationAccountRequest before_validation :normalize_phone - validates :name, :email, :city_town, :country, :province_state, presence: true + validates :name, :email, :requester_name, :city_town, :country, :province_state, presence: true validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i validates :phone_number, presence: true, phone: {possible: true, allow_blank: true} diff --git a/app/services/organizations/create_service.rb b/app/services/organizations/create_service.rb index 8a4f2ebfe..599bba598 100644 --- a/app/services/organizations/create_service.rb +++ b/app/services/organizations/create_service.rb @@ -1,11 +1,12 @@ -# class to create a new location, organization, user, and staff account with role admin -# email is sent to admin user if all steps are successful +# Class to create a new location, organization, user, and staff account with role admin. +# An email is sent to admin user if all steps are successful. +# Be sure to use the Country and State codes from countries_states.yml # call with Organizations::CreateService.new.signal(args) # sample args: # { # location: { -# country: 'Mexico', -# city_town: 'La Ventana', +# country: 'MX', +# city_town: 'JAL', # province_state: 'Baja' # }, # organization: { diff --git a/app/services/organizations/importers/csv_import_service.rb b/app/services/organizations/importers/csv_import_service.rb index 54d78b32f..da0faf5af 100644 --- a/app/services/organizations/importers/csv_import_service.rb +++ b/app/services/organizations/importers/csv_import_service.rb @@ -4,9 +4,10 @@ module Organizations module Importers class CsvImportService Status = Data.define(:success?, :count, :no_match, :errors) - def initialize(file) + def initialize(file, current_user_id) @file = file - @organization = Current.organization + @organization = ActsAsTenant.current_tenant + @current_user = User.find(current_user_id) @count = 0 @no_match = [] @errors = [] @@ -14,51 +15,55 @@ def initialize(file) def call catch(:halt_import) do - validate_file - - CSV.foreach(@file.to_path, headers: true, skip_blanks: true).with_index(2) do |row, index| - # Header may be different depending on which form applicaiton was used(e.g. google forms) or how it was created(User creates form with "Email Address") - email = row[@email_header].downcase - # Google forms uses "Timestamp", other services may use a different header - csv_timestamp = Time.parse(row["Timestamp"]) if row["Timestamp"].present? - - 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 - ActiveRecord::Base.transaction do - create_form_answers(FormSubmission.create!(person:, csv_timestamp:), row) - @count += 1 + @file.download do |data| + validate_file(data) + CSV.parse(data, headers: true, skip_blanks: true).each_with_index do |row, index| + # Header may be different depending on which form applicaiton was used(e.g. google forms) or how it was created(User creates form with "Email Address") + email = row[@email_header].downcase + # Google forms uses "Timestamp", other services may use a different header + csv_timestamp = Time.parse(row["Timestamp"]) if row["Timestamp"].present? + + person = Person.find_by(email:, organization: @organization) + previously_matched_form_submission = FormSubmission.where(person:, csv_timestamp:) + + if person.nil? + @no_match << [index + 2, email] + elsif previously_matched_form_submission.present? + next + else + ActiveRecord::Base.transaction do + create_form_answers(FormSubmission.create!(person:, csv_timestamp:), row) + @count += 1 + end end + rescue => e + @errors << [index + 2, e] end - rescue => e - @errors << [index, e] end end - Status.new(@errors.empty?, @count, @no_match, @errors) + Turbo::StreamsChannel.broadcast_replace_to ["csv_import", @current_user], + target: "results", + partial: "organizations/staff/external_form_upload/upload_results", + locals: { + import: Status.new(@errors.empty?, @count, @no_match, @errors) + } end private - def validate_file + def validate_file(data) raise FileTypeError unless @file.content_type == "text/csv" - - first_row = CSV.foreach(@file.to_path).first - raise FileEmptyError if first_row.nil? + first_row = CSV.new(data).shift 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) + email_headers.each do |header| + @email_header = header if first_row.include?(header) end raise EmailColumnError unless @email_header - rescue FileTypeError, FileEmptyError, TimestampColumnError, EmailColumnError => e - @errors << e + rescue FileTypeError, TimestampColumnError, EmailColumnError => e + @errors << [1, e] throw :halt_import end diff --git a/app/views/layouts/adopter_foster_dashboard.html.erb b/app/views/layouts/adopter_foster_dashboard.html.erb index bb4858c06..e6eaca4b3 100644 --- a/app/views/layouts/adopter_foster_dashboard.html.erb +++ b/app/views/layouts/adopter_foster_dashboard.html.erb @@ -121,11 +121,6 @@ Log Out <% end %> - diff --git a/app/views/layouts/shared/_navbar.html.erb b/app/views/layouts/shared/_navbar.html.erb index 3352787e7..8c4be241c 100644 --- a/app/views/layouts/shared/_navbar.html.erb +++ b/app/views/layouts/shared/_navbar.html.erb @@ -1,15 +1,5 @@