Rails: How to add data header in csv file if different attributes are there in Json? - ruby-on-rails

I have a array of data which is
user: [
{name: 'foo', email: 'foo#gmail.com'},
{name: 'foo', email: 'foo#gmail.com', phone: '76767676'},
{name: 'foo', email: 'foo#gmail.com', pin_code: '22526'},
]
and I am changing it into csv by this code
CSV.open(file, 'w') do |csv|
user_hash = JSON.parse(user.to_json)
csv << user_hash.first.keys
user_hash.each do |hash|
csv << hash.values
end
end
in above code there is a problem that i am taking user_hash.first.keys so first keys name and email will be there for header.
I want to know that how can we add new column in header if a new attribute arrives which is happening in other two.
because I am taking the user_hash.first.keys so name and email header columns will be there not phone and pin_code.
Thanks.

You will need to collect all available keys first and then get the values for those keys in the same order for each hash.
user_hash = JSON.parse(user.to_json)
keys = user_hash.flat_map(&:keys).uniq
CSV.open(file, 'w') do |csv|
csv << keys
user_hash.each { |hash| csv << hash.values_at(*keys) }
end

Related

How to check header exist before import data in Ruby CSV?

I want to write header only 1 time in first row when import data to csv in ruby, but the header is written many time on output file.
job_datas.each do |job_data|
#company_job = job data coverted etc....
save_job_to_csv(#company_job)
end
def save_job_to_csv(job_data)
filepath = "tmp/jobs/jobs.csv"
CSV.open(filepath, "a", :headers => true) do |csv|
if csv.blank?
csv << CompanyJob.attribute_names
end
csv << job_data.attributes.values
end
end
Any one can give me solution? Thank you so much!
You are calling save_job_to_csv the method for each job_data and pushing header every time csv << CompanyJob.attribute_names
filepath = "tmp/jobs/jobs.csv"
CSV.open(filepath, "a", :headers => true) do |csv|
# push header once
csv << CompanyJob.attribute_names
# push every job record
job_datas.each do |job_data|
#company_job = job data coverted etc....
csv << #company_job.attributes.values
end
end
The above script can be created wrapped a method but if you like to write a separate method that just saves the CSV, then you need to refactor the script when you first prepare an array of values holding header and pass it to a method that just saves to CSV.
You could do something similar to this:
def save_job_to_csv(job_data)
filepath = "tmp/jobs/jobs.csv"
unless File.file?(filepath)
File.open(filepath, 'w') do |file|
file.puts(job_data.attribute_names.join(','))
end
end
CSV.open(filepath, "a", :headers => true) do |csv|
csv << job_data.attributes.values
end
end
It just checks beforehand if the file exists and if not it adds the header. If you want tabs as column separators, you just have to change the value for the join function and add the col_sep parameter to CSV.open():
file.puts(job_data.attribute_names.join("\t"))
CSV.open(filepath, "a", :headers => true, col_sep: "\t") do |csv|

Using Model.where with self.id or similar when exporting to .CSV

I am trying to export data to CSV based on the current case_main "show" page the export button is on. I believe I need something similar to the following, but I can't get any permutation of it to work. Any attempt at referencing self.id or :id results in an error or nil.
Billedtime.where(case_main_id: self.id).each do |time|
Using Billedtime.all.each do |time| lets the code run, but it obviously grabs everything instead of only billedtimes for the current case_main.
case_main has two one-to-manys, billedtimes and lineitems. I am trying to combine billedtimes + lineitems for a particular case_main into a single .CSV export.
Here is the code:
Model:
def self.to_csv
desired_columns = ["Client Code",
"Case Name" ,
"Date",
"Description",
"Hours",
"Charge",
]
CSV.generate(headers: true) do |csv|
# Header columns
csv << desired_columns
# Data columns
Billedtime.where(case_main_id: self.id).each do |time|
csv <<
[
time.case_main.client.client_code,
time.case_main.case_name,
time.billedtime_date,
time.billedtime_description,
time.billedtime_hours,
time.billedtime_total
]
end
Lineitem.where(case_main_id: self.id).each do |line|
csv <<
[
line.case_main.client.client_code,
line.case_main.case_name,
line.lineitem_date.to_date,
line.lineitem_description,
' ',
line.lineitem_total
]
end
end
end
Controller:
def show
#case_mains = CaseMain.where(:id => #case_main.id).limit(5)
respond_to do |format|
format.html
format.csv { send_data #case_mains.to_csv, filename: "case-#{Date.today}.csv"}
end
end
View:
<%= link_to "Export Billed Items", case_main_path(format: "csv") %>
Thanks!
The issue is that your model doesn't poses an id attribute, so self.id doesn't do the job. Instead you want to grab the current scope using all and iterate through the collection using each or find_each. To prevent an 1+N query I've also added the needed includes call.
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << [
"Client Code",
"Case Name",
"Date",
"Description",
"Hours",
"Charge"
]
all.includes(:client, :billedtimes, :lineitems).find_each do |case_main|
case_main.billedtimes.each do |billedtime|
csv << [
case_main.client.client_code,
case_main.case_name,
billedtime.billedtime_date,
billedtime.billedtime_description,
billedtime.billedtime_hours,
billedtime.billedtime_total
]
end
case_main.lineitems.each do |lineitem|
csv << [
case_main.client.client_code,
case_main.case_name,
lineitem.lineitem_date.to_date,
lineitem.lineitem_description,
' ',
line.lineitem_total
]
end
end
end
end
You might think, I don't want to export all records. However all (without receiver) operates on the current scope, so when you use it in the context CaseMain.where(id: #case_main.id).limit(5).to_csv you will grab the collection CaseMain.where(id: #case_main.id).limit(5).all.

Rails overwrite CSV

I'm currently using this code:
CSV.open "application.csv", "a+" do |csv|
csv << [ "#{params[:first_name]}", "#{params[:last_name]}","#{params[:company]}","#{params[:email]}", "#{params[:phone]}", "#{params[:business]}", "#{params[:services]}", "#{params[:employees]}", "#{params[:turnover]}" ]
end
Which writes an extra row to the csv each time, what can I put instead of "a+" that will overwrite the entire file each time, so it always only has one row?
You should use 'w' mode. BTW, why do you write "#{params[:first_name]}" where params[:first_name] would be enough? The code should look:
CSV.open 'application.csv', 'w' do |csv|
csv << [params[:first_name], params[:last_name], params[:company], params[:email], params[:phone], params[:business], params[:services], params[:employees], params[:turnover], params[:c4l_services]]
end

Rails: Upload CSV file without header

I followed railscasts #396 Importing CSV and implemented CSV upload in my rails project.
This is my view file:
<%= form_tag import_customers_path, multipart: true do %>
<%= file_field_tag :file %>
<%= submit_tag "Import" %>
<% end %>
This is my controller action:
def import
current_user.customers.import(params[:file])
redirect_to customers_path, notice: "Users imported."
end
And these are my model methods:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << column_names
all.each do |customer|
csv << customer.attributes.values_at(*column_names)
end
end
end
def self.import(file)
CSV.foreach(file.path, headers: true) do |row|
Customer.create! row.to_hash
end
end
Here I don't want user to include header in CSV. When I replace headers: true with headers: false, I get error:
NoMethodError in CustomersController#import
undefined method `to_hash' for ["abc#wer.com"]:Array
Can anybody tell how to upload CSV files without need of header line?
As far as upload and handling of the CSV file goes, you're very, very close. You just have an issue with reading the rows of data to populate the database with, via the Customer.create! call
It looks like you've been testing with a CSV file that only has a single line of data. With the headers: true, that single line was converted to headers and subsequently ignored in the CSV.foreach iterator. So, in effect, you had no data in the file, and no iterations occurred. If you had two rows of data in the input file, you'd have encountered the error, anyway.
Now, when you use headers: false, that line of data is treated as data. And that's where the issue lies: handling the data isn't done correctly.
Since there's no schema in your question, I'll assume a little bit of leeway on fields; you should be able to extrapolate pretty easily to make it work in your situation. This code shows how it works:
CSV.parse(csv_data, headers: false) do |row|
hash = {
first_name: row[0],
last_name: row[1],
age: row[2],
phone: row[3],
address: row[4]
}
Customer.create!(hash)
end
If you wanted a CSV version with headers, this would work well in this case, and has the benefit of not allowing arbitrary access to columns that shouldn't be assigned from an outside source:
CSV.parse(csv_data, headers: true, header_converters: :symbol) do |row|
hash = {
first_name: row[:first_name],
surname: row[:last_name],
birth_year: Date.today - row[:age],
phone: row[:phone],
street_address: row[:address]
}
Customer.create!(hash)
end
Note that the Customer#to_csv in your model is not quite correct, either. First, it creates the CSV file with a header, so you wouldn't be able to export and then import again with this implementation. Next, the header fields variable column_names is not actually defined in this code. Finally, the code doesn't control the order of columns written to the CSV, which means that the headers and values could possibly go out of sync. A correct (non-header) version of this is very simple:
csv_data = CSV.generate do |csv|
csv.each do |customer|
csv << [customer.first_name, customer.last_name, customer.age, customer.phone, customer.address]
end
end
The header-based version is this:
csv_data = CSV.generate do |csv|
csv << ["First Name","Last Name","Age","Phone","Address"]
csv.each do |customer|
csv << [customer.first_name, customer.last_name, customer.age, customer.phone, customer.address]
end
end
Personally, I'd use the header-based version, because it's far more robust, and it's easy to understand which columns are which. If you've ever received a headerless CSV file and had to figure out how to make sense of it without any keys, you'd know why the header is important.
You could just load the CSV file into an array of arrays and remove the first row:
data = CSV.read("path/to/file.csv")
data = data[1..-1]
However this will store the data as an array of values only.
When you use headers: true it uses a hash where the keys are the column header names.

convert array of hashes to csv file

How do you convert an array of hashes to a .csv file?
I have tried
CSV.open("data.csv", "wb") do |csv|
#data.to_csv
end
but it is blank
Try this:
CSV.open("data.csv", "wb") do |csv|
#data.each do |hash|
csv << hash.values
end
end
If you want the first line of the CSV to contain the keys of the hash (a header row), simply do:
CSV.open("data.csv", "wb") do |csv|
csv << #data.first.keys # adds the attributes name on the first line
#data.each do |hash|
csv << hash.values
end
end
Please read the comment of #cgenco below: He wrote a monkey patch for the Array class.
CSV is smart enough to deal with the non-uniform hashes for you. See the code for CSV::Writer#<<
So, this works, and is a bit simpler than the above examples:
CSV.open("data.csv", "wb", {headers: #data.first.keys} ) do |csv|
#data.each do |hash|
csv << hash
end
end
If the keys are not the same in all rows, the current answers fail. This is the safest approach:
data = [{a: 1, b: 2}, {b: 3, c: 4}]
CSV.open("data.csv", "w") { |csv|
headers = data.flat_map(&:keys).uniq
csv << headers
data.each { |row|
csv << row.values_at(*headers)
}
}
All keys will be present in the CSV, even if they don't appear in the first row:
a
b
c
1
2
3
4
If the hashes aren't uniform then you will end up with data in the wrong columns. You should use values_at instead:
CSV.open("data.csv", "wb") do |csv|
keys = #data.first.keys
csv << keys
#data.each do |hash|
csv << hash.values_at(*keys)
end
end
None of the other answers worked for me for one reason or another, so I'll throw in my contribution as well. Here's what worked for me for my array of hashes with ruby 2.7:
headers = data.map(&:keys).flatten.uniq
CSV.open("data.csv", "wb", {headers: headers} ) do |csv|
csv << headers
data.each do |hash|
csv << hash
end
end

Resources