How to convert an array of objects to CSV in Rails - ruby-on-rails

I have an array of objects. I am trying to create CSV data and allow the user to download that file but I get the following error:
Undefined method 'first_name' for Hash:0x007f946fc76590
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
This is the button that allows a user to download the CSV:
<%= link_to "Download Employee CSV", download_employee_csv_path %>
Controller:
def download_employee_csv
employee_csv_data = []
employees.each do |employee|
employee_csv_data << {
first_name: employee[:first_name],
last_name: employee[:last_name],
email: employee_email,
phone1: employee[:phone1],
gender: employee[:gender],
veteran: employee[:veteran].to_s,
dob: employee[:dob],
core_score: service_score,
performance_rank: rank,
industry_modules_passed: industry_modules_passed
}
end
respond_to do |format|
format.html
format.csv { send_data Employer.to_csv(employee_csv_data), filename: "download_employee_csv.csv" }
end
end
employee_csv_data:
=> [{:first_name=>"Christopher",
:last_name=>"Pelnar",
:email=>"pelnar#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1988-09-09",
:core_score=>"No Score",
:performance_rank=>"No Rank",
:industry_modules_passed=>"No Industry Modules Passed"},
{:first_name=>"chris",
:last_name=>"pelnar",
:email=>"chris#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1998-09-09",
:core_score=>"729",
:performance_rank=>"Good",
:industry_modules_passed=>"Entry-Service, Entry-Tech"}]
Model:
def self.to_csv(employee_csv_data)
attributes = %w(first_name last_name email phone gender veteran dob core_score performance_rank industry_modules_passed)
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
When I click the button, it takes me to the blank HTML page without any problem. When I add .csv to the filename in the URL on that page I get the error.

It looks like it's an array of Hashes. To access properties of a hash in Ruby you need to use brackets. Try updating your code to this:
csv << attributes.map{ |attr| obj.send([], attr) }
or more concisely:
csv << attributes.map{ |attr| obj[attr] }
One more thing, in the example you provided, the keys in the hash are symbols which means you may need to convert your attributes to symbols when trying to access them, like this:
csv << attributes.map{ |attr| obj[attr.to_sym] }

I adapted #Ctpelnar1988's answer to determine the attributes dynamically and allow each array item to have different columns:
def array_of_hashes_to_csv(array)
array_keys = array.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << array_keys
array.each do |obj|
csv << array_keys.map{ |attr| obj[attr] }
end
end
end
Example:
puts array_of_hashes_to_csv([
{attr_a: 1, attr_b: 2},
{attr_a: 3, attr_c: 4}
])
attr_a,attr_b,attr_c
1,2,
3,,4
In the more specific "employee_csv_data" context, I think it'd look like this:
def self.to_csv(employee_csv_data)
attributes = employee_csv_data.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map { |attr| obj[attr] }
end
end
end

Related

Ruby on Rails Custom Helper outputting HTML nested list

as in title I'm trying to create helper that does that but I'm struggling. I'm getting errors or simply empty list like this:
And I want to achieve this:
There is to much logic to simply put this code in view. A results is a hash where the key is a website id and value is either an array of bookmarks ids or just bookmark id.
My code:
module WebsitesHelper
def present_search_results(results)
content_tag(:ul, class: "websites-list") do
results.each do |key, value|
website = Website.find(key)
concat(content_tag(:li, website.url, class: "website-#{key}") do
bookmarks = website.bookmarks.select do |b|
if value.is_a?(Array)
value.include?(b.id)
else
value = b.id
end
end
content_tag(:ul, nil, id: "website-#{key}") do
bookmarks.each do |b|
content_tag(:li, b.title)
end
end
end)
end
end
end
end
If you want to stick with helpers, then something like this could help:
def present_search_results(results)
content_tag(:ul, class: "websites-list") do
results.map do |website_id, bookmarks|
bookmarks = [bookmarks] unless bookmarks.is_a?(Array)
content_tag(:li, class: "website-#{website_id}") do
website = Website.find(website_id)
concat(website.url)
concat(
content_tag(:ul, class: "bookmarks-list") do
bookmarks.map do |bookmark_id|
bookmark = Bookmark.find(bookmark_id)
content_tag(:li, bookmark.title)
end.reduce(:+)
end
)
end
end.reduce(:+)
end
end
But, in my opinion, that code is not easy to read, so you could use plain html instead, like this:
def present_search_results(results)
list = "<ul class='websites-list'>"
results.each do |(website_id, bookmarks)|
bookmarks = [bookmarks] unless bookmarks.is_a?(Array)
website = Website.find(website_id)
list += "<li class='website-#{website_id}'>#{website}"
list += "<ul class='bookmarks-list'>"
bookmarks.each do |bookmark_id|
bookmark = Bookmark.find(bookmark_id)
list += "<li>#{bookmark.title}</li>"
end
list += "</ul></li>"
end
list += "</ul>"
list.html_safe
end
I like this one better, since it is easier to read. But both with output the list you want.

Named scope with multiple values

I'm having some trouble with my named scope.
def self.by_status(status)
arr = status.split(',').map{ |s| s }
logger.debug "RESULT: #{arr.inspect}"
where(status: arr)
end
When I call this scope with more than one value, the result of arr = ["New", "Open"]
This does not return any results, while it should. If I try this command in the console: Shipment.where(status: ['New', 'Open']) I get the results that I'm expecting.
Am I missing something here?
Edit (added the call of the class method ):
def self.to_csv(options = {}, vendor_id, status)
CSV.generate(options) do |csv|
csv << column_names
if !vendor_id.blank? && status.blank?
by_vendor_id(vendor_id).each do |product|
csv << product.attributes.values_at(*column_names)
end
elsif !vendor_id.blank? && !status.blank?
by_vendor_id(vendor_id).by_status(status).each do |product|
csv << product.attributes.values_at(*column_names)
end
elsif vendor_id.blank? && !status.blank?
logger.debug "by_status result: #{by_status(status).inspect}"
by_status(status).each do |product|
csv << product.attributes.values_at(*column_names)
end
else
all.each do |product|
csv << product.attributes.values_at(*column_names)
end
end
end
end
Try this in your model:
scope :by_status, ->(*statuses) { where(status: statuses) }
Then in your code you can call:
Shipment.by_status('New', 'Open')
This has the flexibility to just take one argument, too:
Shipment.by_status('New')

Rails 4 - exporting routes to CSV

I'm developing an ecommerce app and I have a csv export feature which exports all product details like name, price, etc. Each product is in one row with a column for each product attribute. I want to add a column to the file which will contain the url of each product. The reason I want this is so I can use this as a product feed that can be submitted to various shopping sites.
Here is my export code in the controller. How do I add a column called route to this? I don't have a route column in the model.
#controller
def productlist
#listings = Listing.all
respond_to do |format|
format.html
format.csv { send_data #listings.to_csv(#listings) }
end
end
#model
def self.to_csv(listings)
wanted_columns = [:sku, :name, :designer_or_brand, :description, :price, :saleprice, :inventory, :category]
CSV.generate do |csv|
csv << ['Product_ID', 'Product_title', 'Designer_or_Brand', 'Description', 'Price', 'SalePrice', 'Quantity_in_stock', 'Category'] + [:Image, :Image2, :Image3, :Image4]
listings.each do |listing|
attrs = listing.attributes.with_indifferent_access.values_at(*wanted_columns)
attrs.push(listing.image.url, listing.image2.try(:url), listing.image3.try(:url), listing.image4.try(:url))
csv << attrs
end
end
end
def self.to_csv(listings)
wanted_columns = [:sku, :name, :designer_or_brand, :description, :price,
:saleprice, :inventory, :category]
header = %w(Product_ID Product_title Designer_or_Brand Description Price
SalePrice Quantity_in_stock Category Image Image2 Image3 Image4 ProductUrl)
CSV.generate do |csv|
csv << header
listings.each do |listing|
attrs = listing.attributes.with_indifferent_access.values_at(*wanted_columns)
<< listing.image.url << listing.image2.try(:url)
<< listing.image3.try(:url) << listing.image4.try(:url)
<< Rails.application.routes.url_helpers.product_url(listing.Product_ID)
csv << attrs
end
end
end
Actually the only difference is last item of array: Rails.application.routes.url_helpers.product_url(listing.Product_ID), where product_url is your route to product#show

How to upload CSV with Paperclip

I have looked over the other posts about creating a CSV with Paperclip but am still a bit lost on why this isn't working. I have a method that generates a CSV string (using CSV.generate), and I try to save it to a Report in the Reports Controller with the following method:
def create(type)
case type
when "Geo"
csv_string = Report.generate_geo_report
end
#report = Report.new(type: type)
#report.csv_file = StringIO.new(csv_string)
if #report.save
puts #report.csv_file_file_name
else
#report.errors.full_messages.to_sentence
end
end
However, upon execution, I get a undefined method 'stringify_keys' for "Geo":String error. Here is the Report model:
class Report < ActiveRecord::Base
attr_accessible :csv_file, :type
has_attached_file :csv_file, PAPERCLIP_OPTIONS.merge(
:default_url => "//s3.amazonaws.com/production-recruittalk/media/avatar-placeholder.gif",
:styles => {
:"259x259" => "259x259^"
},
:convert_options => {
:"259x259" => "-background transparent -auto-orient -gravity center -extent 259x259"
}
)
def self.generate_geo_report
male_count = 0
female_count = 0
csv_string = CSV.generate do |csv|
csv << ["First Name", "Last Name", "Email", "Gender", "City", "State", "School", "Created At", "Updated At"]
Athlete.all.sort_by{ |a| a.id }.each do |athlete|
first_name = athlete.first_name || ""
last_name = athlete.last_name || ""
email = athlete.email || ""
if !athlete.sports.blank?
if athlete.sports.first.name.split(" ", 2).first.include?("Women's")
gender = "Female"
female_count += 1
else
gender = "Male"
male_count += 1
end
else
gender = ""
end
city = athlete.city_id? ? athlete.city.name : ""
state = athlete.state || ""
school = athlete.school_id? ? athlete.school.name : ""
created_at = "#{athlete.created_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.created_at.to_s.strip}"
updated_at = "#{athlete.updated_at.to_date.to_s[0..10].gsub(" ", "0")} #{athlete.updated_at.to_s.strip}"
csv << [first_name, last_name, email, gender, city, state, school, created_at, updated_at]
end
csv << []
csv << []
csv << ["#{male_count}/#{Athlete.count} athletes are men"]
csv << ["#{female_count}/#{Athlete.count} athletes are women"]
csv << ["#{Athlete.count-male_count-female_count}/#{Athlete.count} athletes have not declared a gender"]
end
return csv_string
end
end
This is being called from a cron job rake task:
require 'csv'
namespace :reports do
desc "Geo-report"
task :generate_nightly => :environment do
Report.create("Geo")
end
end
Not sure where to begin on getting this functional. Any suggestions? I've been reading Paperclip's doc but I'm a bit of a newbie to it.
Thank you!
There's a lot going on here :)
First, it looks like you're getting your controller and model confused. In the rake task, Report is the model, but you're calling create as if it was the controller method. Models (aka ActiveRecord classes) take a key/value pair:
Report.create(type: "Geo")
Another issue is that you're using "type" for the name of your column, and this will tell ActiveRecord that you're using single table inheritance. That means that you have subclasses of Report. Unless you really want STI, you should rename this column.
Finally, you shouldn't have a controller method that takes an argument. I'm not really sure what you're trying to do there, but controller get their arguments via the params hash.

Undefined Method when running rspec test using a stub

I have a model which has a to_csv method and import method, trying to test this in rspec to make sure it does the right thing but having a problem. I get the following error:
Failures:
1) Category Class import should create a new record if id does not exist
Failure/Error: Category.import("filename", product)
NoMethodError:
undefined method `path' for "filename":String
Model:
class Category
...<snip>
def self.import(file, product)
product = Product.find(product)
CSV.foreach(file.path, headers: true, col_sep: ";") do |row|
row = row.to_hash
row["variations"] = row["variations"].split(",").map { |s| s.strip }
category = product.categories.find(row["id"]) || Category.new(row)
if category.new_record?
product.categories << category
else
category.update_attributes(row)
end
end
end
def self.to_csv(product, options = {})
product = Product.find(product)
CSV.generate(col_sep: ";") do |csv|
csv << ['id','title','description','variations']
product.categories.each do |category|
variations = category.variations.join(',')
csv << [category.id, category.title, category.description, variations]
end
end
end
end
My Test:
describe Category do
describe 'Class' do
subject { Category }
it { should respond_to(:import) }
it { should respond_to(:to_csv) }
let(:data) { "id;title;description;variations\r1;a title;;abd" }
describe 'import' do
it "should create a new record if id does not exist" do
product = create(:product)
File.stub(:open).with("filename","rb") { StringIO.new(data) }
Category.import("filename", product)
end
end
end
end
I would just make Category.import take a filename:
Category.import("filename", product)
Then Category.import just passes this filename to the CSV.foreach call:
CSV.foreach(filename, headers: true, col_sep: ";") do |row|
It's then not necessary to stub File.open or any of that jazz.

Resources