From cd9f20747c6ef541c4298ddc6a6828abdef52901 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Thu, 10 Oct 2024 15:14:38 -0400 Subject: [PATCH] Allow inline account creation when importing CSV (#1291) * Allow inline account creation when importing CSV * Sanitize numeric inputs for CSV * CSV import date validation * Lint fix --- app/models/import.rb | 15 ++++++++++++--- app/models/import/row.rb | 12 ++++++++++-- app/views/import/cleans/show.html.erb | 2 +- app/views/import/confirms/_mappings.html.erb | 14 +++++++++++--- app/views/import/confirms/show.html.erb | 2 +- config/locales/views/imports/en.yml | 9 +++++++++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/app/models/import.rb b/app/models/import.rb index c77ed6c7..04a9b09e 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -71,10 +71,10 @@ class Import < ApplicationRecord { account: row[account_col_label].to_s, date: row[date_col_label].to_s, - qty: row[qty_col_label].to_s, + qty: sanitize_number(row[qty_col_label]).to_s, ticker: row[ticker_col_label].to_s, - price: row[price_col_label].to_s, - amount: row[amount_col_label].to_s, + price: sanitize_number(row[price_col_label]).to_s, + amount: sanitize_number(row[amount_col_label]).to_s, currency: (row[currency_col_label] || default_currency).to_s, name: (row[name_col_label] || default_row_name).to_s, category: row[category_col_label].to_s, @@ -113,6 +113,10 @@ class Import < ApplicationRecord cleaned? && mappings.all?(&:valid?) end + def requires_account? + family.accounts.empty? && mappings.accounts.where(key: "").any? + end + private def import! # no-op, subclasses can implement for customization of algorithm @@ -134,4 +138,9 @@ class Import < ApplicationRecord converters: [ ->(str) { str&.strip } ] ) end + + def sanitize_number(value) + return "" if value.nil? + value.gsub(/[^\d.]/, "") + end end diff --git a/app/models/import/row.rb b/app/models/import/row.rb index 8d434925..d4316a60 100644 --- a/app/models/import/row.rb +++ b/app/models/import/row.rb @@ -4,7 +4,7 @@ class Import::Row < ApplicationRecord validates :amount, numericality: true, allow_blank: true validates :currency, presence: true - validate :date_matches_user_format + validate :date_valid validate :required_columns validate :currency_is_valid @@ -54,13 +54,21 @@ class Import::Row < ApplicationRecord end end - def date_matches_user_format + def date_valid return if date.blank? parsed_date = Date.strptime(date, import.date_format) rescue nil if parsed_date.nil? errors.add(:date, "must exactly match the format: #{import.date_format}") + return + end + + min_date = Account::Entry.min_supported_date + max_date = Date.current + + if parsed_date < min_date || parsed_date > max_date + errors.add(:date, "must be between #{min_date} and #{max_date}") end end diff --git a/app/views/import/cleans/show.html.erb b/app/views/import/cleans/show.html.erb index ef0c230f..25276fb9 100644 --- a/app/views/import/cleans/show.html.erb +++ b/app/views/import/cleans/show.html.erb @@ -23,7 +23,7 @@
<%= lucide_icon "alert-triangle", class: "w-4 h-4 text-red-500" %> -

You have errors in your data

+

<%= t(".errors_notice") %>

diff --git a/app/views/import/confirms/_mappings.html.erb b/app/views/import/confirms/_mappings.html.erb index 9e86a65d..1c798931 100644 --- a/app/views/import/confirms/_mappings.html.erb +++ b/app/views/import/confirms/_mappings.html.erb @@ -3,12 +3,20 @@ <% mappings = mapping_class.for_import(import) %> <% is_last_step = step_idx == import.mapping_steps.count - 1 %> +<% if import.requires_account? && mapping_class == Import::AccountMapping %> +
+ <%= tag.p t(".no_accounts"), class: "text-sm" %> + + <%= link_to t(".create_account"), new_account_path, class: "btn btn--primary whitespace-nowrap", data: { turbo_frame: :modal } %> +
+<% end %> +
-

CSV <%= mapping_label(mapping_class) %>

-

Maybe <%= mapping_label(mapping_class) %>

-

Rows

+

<%= t(".csv_mapping_label", mapping: mapping_label(mapping_class)) %>

+

<%= t(".maybe_mapping_label", mapping: mapping_label(mapping_class)) %>

+

<%= t(".rows_label") %>

diff --git a/app/views/import/confirms/show.html.erb b/app/views/import/confirms/show.html.erb index ae87907d..5d90a91e 100644 --- a/app/views/import/confirms/show.html.erb +++ b/app/views/import/confirms/show.html.erb @@ -28,6 +28,6 @@
-
+
<%= render partial: "import/confirms/mappings", locals: { import: @import, mapping_class: step_mapping_class, step_idx: step_idx } %>
diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml index 5fee0c64..85bebab8 100644 --- a/config/locales/views/imports/en.yml +++ b/config/locales/views/imports/en.yml @@ -4,12 +4,21 @@ en: cleans: show: description: Edit your data in the table below. Red cells are invalid. + errors_notice: You have errors in your data. Hover over the error to see details. title: Clean your data configurations: show: description: Select the columns that correspond to each field in your CSV. title: Configure your import confirms: + mappings: + create_account: Create account + csv_mapping_label: "%{mapping} in CSV" + maybe_mapping_label: "%{mapping} in Maybe" + no_accounts: You don't have any accounts yet. Please create an account that + we can use for (unassigned) rows in your CSV or go back to the Clean step + and provide an account name we can use. + rows_label: Rows show: account_mapping_description: Assign all of your imported file's accounts to Maybe's existing accounts. You can also add new accounts or leave them