Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Allow other import files #1099

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def load

def upload_csv
begin
@import.raw_csv_str = import_params[:raw_csv_str].read
@import.raw_file_str = import_params[:raw_file_str].read
rescue NoMethodError
flash.now[:alert] = "Please select a file to upload"
render :load, status: :unprocessable_entity and return
Expand Down Expand Up @@ -113,6 +113,6 @@ def set_import
end

def import_params(permitted_mappings = nil)
params.require(:import).permit(:raw_csv_str, column_mappings: permitted_mappings, csv_update: [ :row_idx, :col_idx, :value ])
params.require(:import).permit(:raw_file_str, column_mappings: permitted_mappings, csv_update: [ :row_idx, :col_idx, :value ])
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["input", "preview", "submit", "filename", "filesize"]
static values = {
acceptedTypes: Array, // ["text/csv", "application/csv", ".csv"]
acceptedExtension: String, // "csv"
unacceptableTypeLabel: String, // "Only CSV files are allowed."
};

connect() {
this.submitTarget.disabled = true
Expand Down Expand Up @@ -30,15 +35,19 @@ export default class extends Controller {
event.currentTarget.classList.remove("bg-gray-100")

const file = event.dataTransfer.files[0]
if (file && this._isCSVFile(file)) {
if (file && this._formatAcceptable(file)) {
this._setFileInput(file);
this._fileAdded(file)
} else {
this.previewTarget.classList.add("text-red-500")
this.previewTarget.textContent = "Only CSV files are allowed."
this.previewTarget.textContent = this.unacceptableTypeLabelValue
}
}

click() {
this.inputTarget.click();
}

// Private

_fetchFileSize(size) {
Expand All @@ -57,7 +66,7 @@ export default class extends Controller {
if (file) {
if (file.size > fileSizeLimit) {
this.previewTarget.classList.add("text-red-500")
this.previewTarget.textContent = "File size exceeds the limit of 5MB"
this.previewTarget.textContent = this.unacceptableTypeLabelValue
return
}

Expand All @@ -80,10 +89,9 @@ export default class extends Controller {
}
}

_isCSVFile(file) {
const acceptedTypes = ["text/csv", "application/csv", ".csv"]
_formatAcceptable(file) {
const extension = file.name.split('.').pop().toLowerCase()
return acceptedTypes.includes(file.type) || extension === "csv"
return this.acceptedTypesValue.includes(file.type) || extension === this.acceptedExtensionValue
}

_setFileInput(file) {
Expand Down
20 changes: 10 additions & 10 deletions app/models/import.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class Import < ApplicationRecord
belongs_to :account

validate :raw_csv_must_be_parsable
validate :raw_file_must_be_parsable
validates :col_sep, inclusion: { in: Csv::COL_SEP_LIST }

before_save :initialize_csv, if: :should_initialize_csv?
Expand All @@ -19,7 +19,7 @@ def publish_later
end

def loaded?
raw_csv_str.present?
raw_file_str.present?
end

def configured?
Expand Down Expand Up @@ -88,16 +88,16 @@ def get_normalized_csv_with_validation
end

def get_raw_csv
return nil if raw_csv_str.nil?
Import::Csv.new(raw_csv_str, col_sep:)
return nil if raw_file_str.nil?
Import::Csv.new(raw_file_str, col_sep:)
end

def should_initialize_csv?
raw_csv_str_changed? || column_mappings_changed?
raw_file_str_changed? || column_mappings_changed?
end

def initialize_csv
generated_csv = generate_normalized_csv(raw_csv_str)
generated_csv = generate_normalized_csv(raw_file_str)
self.normalized_csv_str = generated_csv.table.to_s
end

Expand Down Expand Up @@ -175,12 +175,12 @@ def define_column_mapping_keys
end
end

def raw_csv_must_be_parsable
def raw_file_must_be_parsable
begin
CSV.parse(raw_csv_str || "", col_sep:)
CSV.parse(raw_file_str || "", col_sep:)
rescue CSV::MalformedCSVError
# i18n-tasks-use t('activerecord.errors.models.import.attributes.raw_csv_str.invalid_csv_format')
errors.add(:raw_csv_str, :invalid_csv_format)
# i18n-tasks-use t('activerecord.errors.models.import.attributes.raw_file_str.invalid_csv_format')
errors.add(:raw_file_str, :invalid_csv_format)
end
end
end
4 changes: 2 additions & 2 deletions app/models/import/csv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def self.parse_csv(csv_str, col_sep: DEFAULT_COL_SEP)
)
end

def self.create_with_field_mappings(raw_csv_str, fields, field_mappings, col_sep = DEFAULT_COL_SEP)
raw_csv = self.parse_csv(raw_csv_str, col_sep:)
def self.create_with_field_mappings(raw_file_str, fields, field_mappings, col_sep = DEFAULT_COL_SEP)
raw_csv = self.parse_csv(raw_file_str, col_sep:)

generated_csv_str = CSV.generate headers: fields.map { |f| f.key }, write_headers: true, col_sep: do |csv|
raw_csv.each do |row|
Expand Down
4 changes: 2 additions & 2 deletions app/views/imports/_csv_paste.html.erb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<%= styled_form_with model: @import, url: load_import_path(@import), class: "space-y-4" do |form| %>
<%= form.text_area :raw_csv_str,
<%= form.text_area :raw_file_str,
rows: 10,
required: true,
placeholder: "Paste your CSV file contents here",
class: "rounded-md w-full border text-sm border-alpha-black-100 bg-white placeholder:text-gray-400" %>

<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-gray-900 text-white text-sm font-medium", data: { turbo_confirm: (@import.raw_file_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
<% end %>

<div class="bg-alpha-black-25 rounded-xl p-1 mt-5">
Expand Down
14 changes: 7 additions & 7 deletions app/views/imports/_csv_upload.html.erb
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
<%= styled_form_with model: @import, url: upload_import_path(@import), class: "dropzone space-y-4", data: { controller: "csv-upload" }, method: :patch, multipart: true do |form| %>
<%= styled_form_with model: @import, url: upload_import_path(@import), class: "dropzone space-y-4", data: { controller: "import-upload", import_upload_accepted_types_value: ["text/csv", "application/csv", ".csv"], import_upload_extension_value: "csv", import_upload_unacceptable_type_label_value: t(".allowed_filetypes") }, method: :patch, multipart: true do |form| %>
<div class="flex items-center justify-center w-full">
<label for="import_raw_csv_str" class="csv-drop-box flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50" data-action="dragover->csv-upload#dragover dragleave->csv-upload#dragleave drop->csv-upload#drop">
<label for="import_raw_file_str" class="raw-file-drop-box flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50" data-action="dragover->import-upload#dragover dragleave->import-upload#dragleave drop->import-upload#drop click->import-upload#click">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<%= lucide_icon "plus", class: "w-5 h-5 text-gray-500" %>
<%= form.file_field :raw_csv_str, class: "hidden", direct_upload: false, accept: "text/csv,.csv,application/csv", data: { csv_upload_target: "input", action: "change->csv-upload#addFile" } %>
<%= form.file_field :raw_file_str, class: "hidden", direct_upload: false, accept: "text/csv,.csv,application/csv", data: { import_upload_target: "input", action: "change->import-upload#addFile" } %>
<p class="mb-2 text-sm text-gray-500 mt-3">Drag and drop your csv file here or <span class="text-black">click to browse</span></p>
<p class="text-xs text-gray-500">CSV (Max. 5MB)</p>
<div class="csv-preview" data-csv-upload-target="preview"></div>
<div class="csv-preview" data-import-upload-target="preview"></div>
</div>
</label>
</div>
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-alpha-black-25 text-gray text-sm font-medium", data: { csv_upload_target: "submit", turbo_confirm: (@import.raw_csv_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
<%= form.submit t(".next"), class: "px-4 py-2 mb-4 block w-full rounded-lg bg-alpha-black-25 text-gray text-sm font-medium", data: { import_upload_target: "submit", turbo_confirm: (@import.raw_file_str? ? { title: t(".confirm_title"), body: t(".confirm_body"), accept: t(".confirm_accept") } : nil) } %>
<% end %>

<div id="template-preview" class="hidden">
<div class="flex flex-col items-center justify-center">
<%= lucide_icon "file-text", class: "w-10 h-10 pt-2 text-black" %>
<div class="flex flex-row items-center justify-center gap-0.5">
<div><span data-csv-upload-target="filename"></span></div>
<div><span data-csv-upload-target="filesize" class="font-semibold"></span></div>
<div><span data-import-upload-target="filename"></span></div>
<div><span data-import-upload-target="filesize" class="font-semibold"></span></div>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion config/locales/models/import/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ en:
models:
import:
attributes:
raw_csv_str:
raw_file_str:
invalid_csv_format: is not a valid CSV format
1 change: 1 addition & 0 deletions config/locales/views/imports/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ en:
"inflow" (income)
requirement3: Can have 0 or more tags separated by |
csv_upload:
allowed_filetypes: Only CSV files are allowed.
confirm_accept: Yep, start over!
confirm_body: This will reset your import. Any changes you have made to the
CSV will be erased.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class RenameImportRawCsvStrToRawFileStr < ActiveRecord::Migration[7.2]
def change
rename_column :imports, :raw_csv_str, :raw_file_str
end
end
4 changes: 2 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions test/controllers/imports_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
@empty_import = imports(:empty_import)

@loaded_import = @empty_import.dup
@loaded_import.update! raw_csv_str: valid_csv_str
@loaded_import.update! raw_file_str: valid_csv_str

@completed_import = imports(:completed_import)
end
Expand Down Expand Up @@ -59,7 +59,7 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
end

test "should save raw CSV if valid" do
patch load_import_url(@empty_import), params: { import: { raw_csv_str: valid_csv_str } }
patch load_import_url(@empty_import), params: { import: { raw_file_str: valid_csv_str } }

assert_redirected_to configure_import_path(@empty_import)
assert_equal "Import CSV loaded", flash[:notice]
Expand All @@ -71,32 +71,32 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
valid_csv_str.split("\n").each { |row| csv << row.split(",") }
end

patch upload_import_url(@empty_import), params: { import: { raw_csv_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
patch upload_import_url(@empty_import), params: { import: { raw_file_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
assert_redirected_to configure_import_path(@empty_import)
assert_equal "CSV File loaded", flash[:notice]
end
end

test "should flash error message if invalid CSV input" do
patch load_import_url(@empty_import), params: { import: { raw_csv_str: malformed_csv_str } }
patch load_import_url(@empty_import), params: { import: { raw_file_str: malformed_csv_str } }

assert_response :unprocessable_entity
assert_equal "Raw csv str is not a valid CSV format", flash[:alert]
assert_equal "Raw file str is not a valid CSV format", flash[:alert]
end

test "should flash error message if invalid CSV file upload" do
Tempfile.open([ "transactions.csv", ".csv" ]) do |temp|
temp.write(malformed_csv_str)
temp.rewind

patch upload_import_url(@empty_import), params: { import: { raw_csv_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
patch upload_import_url(@empty_import), params: { import: { raw_file_str: Rack::Test::UploadedFile.new(temp, ".csv") } }
assert_response :unprocessable_entity
assert_equal "Raw csv str is not a valid CSV format", flash[:alert]
assert_equal "Raw file str is not a valid CSV format", flash[:alert]
end
end

test "should flash error message if no fileprovided for upload" do
patch upload_import_url(@empty_import), params: { import: { raw_csv_str: nil } }
patch upload_import_url(@empty_import), params: { import: { raw_file_str: nil } }
assert_response :unprocessable_entity
assert_equal "Please select a file to upload", flash[:alert]
end
Expand Down Expand Up @@ -158,7 +158,7 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest
end

test "should redirect back to clean if data is invalid" do
@empty_import.update! raw_csv_str: valid_csv_with_invalid_values
@empty_import.update! raw_file_str: valid_csv_with_invalid_values

get confirm_import_url(@empty_import)
assert_equal "You have invalid data, please fix before continuing", flash[:alert]
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/imports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ completed_import:
name: name
category: category
amount: amount
raw_csv_str: |
raw_file_str: |
date,name,category,tags,amount
2024-01-01,Starbucks drink,Food & Drink,Test Tag,-20
normalized_csv_str: |
Expand Down
2 changes: 1 addition & 1 deletion test/jobs/import_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class ImportJobTest < ActiveJob::TestCase

test "import is published" do
import = imports(:empty_import)
import.update! raw_csv_str: valid_csv_str
import.update! raw_file_str: valid_csv_str

assert import.pending?

Expand Down
8 changes: 4 additions & 4 deletions test/models/import/csv_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Import::CsvTest < ActiveSupport::TestCase

fields = [ date_field, name_field ]

raw_csv_str = <<-ROWS
raw_file_str = <<-ROWS
date,Custom Field Header,extra_field
invalid_date_value,Starbucks drink,Food
2024-01-02,Amazon stuff,Shopping
Expand All @@ -82,7 +82,7 @@ class Import::CsvTest < ActiveSupport::TestCase
"name" => "Custom Field Header"
}

csv = Import::Csv.create_with_field_mappings(raw_csv_str, fields, mappings)
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings)

assert_equal %w[ date name ], csv.table.headers
assert_equal 2, csv.table.size
Expand All @@ -101,7 +101,7 @@ class Import::CsvTest < ActiveSupport::TestCase

fields = [ date_field, name_field ]

raw_csv_str = <<-ROWS
raw_file_str = <<-ROWS
date;Custom Field Header;extra_field
invalid_date_value;Starbucks drink;Food
2024-01-02;Amazon stuff;Shopping
Expand All @@ -111,7 +111,7 @@ class Import::CsvTest < ActiveSupport::TestCase
"name" => "Custom Field Header"
}

csv = Import::Csv.create_with_field_mappings(raw_csv_str, fields, mappings, ";")
csv = Import::Csv.create_with_field_mappings(raw_file_str, fields, mappings, ";")

assert_equal %w[ date name ], csv.table.headers
assert_equal 2, csv.table.size
Expand Down
Loading