How do I Export a CSV file where it includes all the nested attributes?
Greetings:
I would like to export a CSV file where it includes all the nested attributes. I have an invoice model and a product model. Each invoice has many products. I am able to export to CSV following the GoRails video, but how do I include the data from the products page?
I'm fairly new to Rails so any help would be appreciated.
Many thanks,
Steve
Here's my code
class Invoice < ApplicationRecord
has_many :products, dependent: :destroy
def self.to_csv
attributes = %w{Date Vendor_name Invoice_number}
CSV.generate(headers: true) do |csv|
csv << product.attributes.values
all.each do |product|
csv << product.attributes.values_at(*attributes)
end
end
end
end
<%= link_to "Export to CSV", invoices_path(@invoices, format: :csv), class: 'btn btn-info' %>
def index
@invoices = Invoice.all
respond_to do |format|
format.html
format.csv { send_data @invoices.to_csv, filename: "users-#{Date.today}.csv" }
end
end
Hi Jack,
Thank you for your response. Your idea is a good one, but I'm having problems calling the method. I'm assuming I put the method definition in the invoice controller, right? It doesn't seem to populate the product data.
Steve
If the attribute names don't overlap, you could just use a Hash#merge on the attributes before you append them to the CSV.
For example, just change:
all.each do |product|
csv << product.attributes.values_at(*attributes)
end
To:
all.each do |product|
csv << product.attributes.merge(product.vendor.attributes).values_at(*attributes)
end
Where product.vendor.attributes
would be the attributes for your nested model.
Since you're new to Rails, you may ask why this works.
The product
object is an ActiveRecord model object but when you call .attributes
on it, Rails will return the values as a Hash. This allows you to use Ruby and Rails Hash methods on it.
You can confirm the object class by opening your Rails console and running .class
on the objects.
You can merge as many hashes as you need to for this. Just append .merge
to it right before the .values_at
method. If you start doing that, I'd recommend cleaning your code up by moving the merges to a new line and storing them in a variable. Then, you can call .values_at
on that variable.
Hi John,
Many thanks. I'm somehow still getting an error that says "undefined method `title' for "
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << attributes = %w{date vendor_name invoice_number title }
all.each do |product|
csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
end
end
end
I really appreciate your detailed explanation on how ActiveRecord works. It's very clear.
Steve
You're most welcome! :)
Would you be able to post the stack trace that you're receiving with that error?
An undefined method would usually mean a couple of things. Here's what's common:
- You have a typo in the association name that you're calling.
- That association doesn't yet exist. For example, one of your products doesn't have a title yet. If that's that case, you'd usually receive an "Undefined method 'title' for NilClass" <-- I'm betting this is that case here.
You may be able to find this error by adding a couple of logger lines right below the all.each do |product|
line.
For example:
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << attributes = %w{date vendor_name invoice_number title }
all.each do |product|
Rails.logger.debug "Last Product: #{product.id}"
Rails.logger.debug "Last Product Title: #{product.title}"
csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
end
end
end
This will iterate over each product and allow you to see the last product in the iteration that caused the error.
By the way, what is the title object association for your product? What attributes? What type of association is it? etc.
Hi John,
When I put the code you gave me. The error I get is like this (assuming this is the stack trace):
NoMethodError in InvoicesController#index
all.each do |product| Rails.logger.debug "Last Product: #{product.id}" *** Rails.logger.debug "Last Product: #{product.title}"*** csv << product.attributes.merge(product.title.attributes).values_at(*attributes) end end
The attributes for products are sku, title, price, description, and quantity. In my product.rb model page, I got belongs_to :invoice. As follows:
class Product < ApplicationRecord
belongs_to :invoice
end
invoice.rb
class Invoice < ApplicationRecord
has_many :products, dependent: :destroy
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << attributes = %w{date vendor_name invoice_number title }
all.each do |product|
Rails.logger.debug "Last Product: #{product.id}"
Rails.logger.debug "Last Product: #{product.title}"
csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
end
end
end
invoices_controller.rb
def index
@invoices = Invoice.order(created_at: :desc)
respond_to do |format|
format.html
format.csv { send_data @invoices.to_csv, filename: "users-#{Date.today}.csv" }
end
end
I think you're right. It's probably the case I'm not associating it. I'm just not sure what's wrong though. I've tried to fiddle with the controller, but no luck. Thanks again.
Steve
A stack trace is the entire error displayed in your application log. Here's an example of the stack trace that rails provides in the browser: https://i.stack.imgur.com/yrraB.png
I see what the issue is here. title
is not an ActiveRecord Model object. It's an ActiveRecord model attribute for Product
. With that said you won't be able to call #attibutes
on it like: product.title.attributes
. You should already be outputing the title
into your CSV file with your code if you remove the merge.
Since, Invoice
is the only association you have on your Product
model, that's the only other object you can run #attributes
on. For instance: product.invoice.attributes
.
Interesting. Would you please check your rails server logs to see how many "Last Product" lines processed?
In another terminal window, start the rails console via rails console
and run Product
. Please paste in what that says here.
When I just run Product, it says:
"Product (call 'Product.connection' to establish a connection)"
When I hit Product.last, it does output an entry.
=> #
Hmm I don't think the entry example you mentioned pasted in correctly. Would you be able to try again?
oh sorry, not sure why it did that.
Product id: 3, sku: "1234", title: "20inch Monitor", description: "used", price: "60", quantity: 2, invoice_id: 3, created_at: "2017-08-22 12:37:21", updated_at: "2017-08-22 12:37:21"
Hey Steve, did you add the merge for the title because you weren't seeing the title in your CSV file? What results are you hoping for and what are you receiving?
I can't seem to get any of my nested attributes like "title", "description", or "price." I'm hoping to export the file and then see the nested data that I inputed in the CSV file. To use the example above, I should see "20inch Monitor" for the title. I'm not receiving anything. Basically the CSV file is blank. I see the headings for title and all, but it's not populating the entries.
Hey Steve,
Just a real quick suggestion - use the byebug gem to figure out what's going on at that moment, see below where I added byebug
class Invoice < ApplicationRecord
has_many :products, dependent: :destroy
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << attributes = %w{date vendor_name invoice_number title }
all.each do |product|
byebug
csv << product.attributes.merge(product.title.attributes).values_at(*attributes)
end
end
end
Now in your console when you run the method, it will halt execution at that point and give you a console where you can start seeing what you're working with. So for example, if you type product
in the console, you'll see the current product object for that iteration. From here you can start figuring out exactly what you need to do to get access to your associated objects.
If it only occurs on a certain record, you can do stuff like byebug if product.id == 24
so you don't have to manually continue
over all the "good" objects.
Outside of this, could you provide a simplified repo so we can see what you're seeing and tinker around a bit?
Hi Steve, I see what the problem is. You CSV iterator is built for a has_one or belongs_to relationship.
In a has_one or belongs_to relationship, Active record will pass the object in directly. Which allows you to call something like:
Invoice.first.product
# returns object
However, since you have a has_many
relationship in your Invoice
model, you need to treat the returned value as an array.
For example:
Invoice.first.products
# returns an array of objects
You need to change how your iteration works.
First, you're iterating through invoices, not products.
Change all.each do |product|
to all.each do |invoice|
.
Then, you need to add another iterator because an invoice has_many
products. That means, you need to go through each one of those products.
invoice.products.each do |product|
Next, you can place those products into the CSV file.
For example:
class Invoice < ApplicationRecord
has_many :products, dependent: :destroy
def self.to_csv
CSV.generate(headers: true) do |csv|
csv << attributes = %w{date vendor_name invoice_number title }
all.each do |invoice|
invoice.products.each do |product|
csv << invoice.attributes.merge(product.attributes).values_at(*attributes)
end
end
end
end
Give that a shot and let me know if it works.