I have the following method on on my action to export to excel a list of user:
def users_report
#users = Kid.where(confirmation_token: nil).paginate(:page => params[:page], :per_page => 30)
#userxls = Kid.where(confirmation_token: nil)
respond_to do |format|
format.html
format.xls { send_data #userxls.to_csv({col_sep: "\t"}) }
end
end
On my model the to_csv method:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << ["Name", "Surname", "E-mail", "Age", "School", "Class", "Native Language", "Practised Language", "Conversations same Native", "Convserations different Native", "Message same Native", "Message different Native", "Posts", "Clossed/Finished calls", "Missed calls", "Connections per Week", "Nb of foreign friends", "Nb of friends same country", "Activation Date", "Email Parent", "Parent Activated"]
kids = Array.new
all.each do |kid|
if kid.user_role != "admin"
k = Array.new
kid.name = kid.name rescue "No name"
k << kid.name
kid.surname = kid.surname rescue "No surname"
k << kid.surname
kid.email = kid.email rescue "No email"
k << kid.email
k << kid.age rescue "No age"
if !kid.school.nil?
k << kid.school.name
else
k << "No School"
end
if kid.courses.empty?
k << "No Courses"
else
k << kid.courses.first.name
end
if !kid.native_languages.empty?
languages = Array.new
kid.native_languages.each do |lang|
languages << Language.find(lang).name
end
k << languages
else
k << "No native language"
end
if !kid.practice_languages.empty?
languages = Array.new
kid.practice_languages.each do |lang|
languages << Language.find(lang).name
end
k << languages
else
k << "No practise language"
end
k << kid.number_of_native_conversations rescue "0"
k << kid.number_of_foreign_conversations rescue "0"
k << kid.number_of_native_messages rescue "0"
k << kid.number_of_foreign_messages rescue "0"
k << kid.number_of_activity_posts rescue "0"
k << kid.number_of_finished_closed_calls rescue "0"
k << kid.number_of_missed_calls rescue "0"
k << kid.avg_of_connections_week rescue "0"
k << kid.number_of_foreign_friends rescue "0"
k << kid.number_of_friends_same_country rescue "0"
k << kid.confirmed_at.try(:strftime, "%d/%m/%Y") rescue "0"
k << kid.tutor.email rescue "No parent email"
k << kid.tutor.confirmed? rescue "No parent email"
kids << k
end
end
kids.each do |k|
csv << k
end
end
end
But on my excel file I'm getting names like Jérôme instead of Jérôme. I tried:
# encoding: utf-8
on my view also tried for every field
.force_encoding("UTF-8")
But I still have this problem. Please I really need help with this.
Thanks in advance
CVS::generate understands an option :encoding (see Ruby API).
So use
format.xls { send_data #userxls.to_csv({col_sep: "\t", encoding: 'UTF-8'}) }
You may also think about separating representation and business logic. I use csv_builder that provides views like user_report.csv.csvbuilder to define the csv output.
cvs_builder uses the instance variable #encoding to specify the output character encoding.
Edit
It seems, like your generated csv is already encoded in UTF-8 but you read it as if it were ISO-8859-1 aka. LATIN-1.
You may want to try to generate the csv in LATIN-1 as excel has issues importing UTF-8 csv files.
Depending on the Ruby or Rails version, you have to use ISO-8859-1 instead of LATIN-1.
Related
I am new in ruby so please forgive the noobishness.
I have a CSV with two columns. One for animal name and one for animal type.
I have a hash with all the keys being animal names and the values being animal type. I would like to write the hash to the CSV without using fasterCSV. I have thought of several ideas what would be easiest.. here is the basic layout.
require "csv"
def write_file
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "wb") do |csv|
csv << [???????????]
end
end
When I opened the file to read from it I opened it File.open("blabla.csv", headers: true)
Would it be possible to write back to the file the same way?
If you want column headers and you have multiple hashes:
require 'csv'
hashes = [{'a' => 'aaaa', 'b' => 'bbbb'}]
column_names = hashes.first.keys
s=CSV.generate do |csv|
csv << column_names
hashes.each do |x|
csv << x.values
end
end
File.write('the_file.csv', s)
(tested on Ruby 1.9.3-p429)
Try this:
require 'csv'
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "wb") {|csv| h.to_a.each {|elem| csv << elem} }
Will result:
1.9.2-p290:~$ cat data.csv
dog,canine
cat,feline
donkey,asinine
I think the simplest solution to your original question:
def write_file
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
CSV.open("data.csv", "w", headers: h.keys) do |csv|
csv << h.values
end
end
With multiple hashes that all share the same keys:
def write_file
hashes = [ { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' },
{ 'dog' => 'rover', 'cat' => 'kitty', 'donkey' => 'ass' } ]
CSV.open("data.csv", "w", headers: hashes.first.keys) do |csv|
hashes.each do |h|
csv << h.values
end
end
end
CSV can take a hash in any order, exclude elements, and omit a params not in the HEADERS
require "csv"
HEADERS = [
'dog',
'cat',
'donkey'
]
def write_file
CSV.open("data.csv", "wb", :headers => HEADERS, :write_headers => true) do |csv|
csv << { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
csv << { 'dog' => 'canine'}
csv << { 'cat' => 'feline', 'dog' => 'canine', 'donkey' => 'asinine' }
csv << { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine', 'header not provided in the options to #open' => 'not included in output' }
end
end
write_file # =>
# dog,cat,donkey
# canine,feline,asinine
# canine,,
# canine,feline,asinine
# canine,feline,asinine
This makes working with the CSV class more flexible and readable.
I tried the solutions here but got an incorrect result (values in wrong columns) since my source is a LDIF file that not always has all the values for a key. I ended up using the following.
First, when building up the hash I remember the keys in a separate array which I extend with the keys that are not allready there.
# building up the array of hashes
File.read(ARGV[0]).each_line do |lijn|
case
when lijn[0..2] == "dn:" # new record
record = {}
when lijn.chomp == '' # end record
if record['telephonenumber'] # valid record ?
hashes << record
keys = keys.concat(record.keys).uniq
end
when ...
end
end
The important line here is keys = keys.concat(record.keys).uniq which extends the array of keys when new keys (headers) are found.
Now the most important: converting our hashes to a CSV
CSV.open("export.csv", "w", {headers: keys, col_sep: ";"}) do |row|
row << keys # add the headers
hashes.each do |hash|
row << hash # the whole hash, not just the array of values
end
end
[BEWARE] All the answers in this thread are assuming that the order of the keys defined in the hash will be constant amongst all rows.
To prevent problems (that I am facing right now) where some values are assigned to the wrong keys in the csv (Ex:)
hahes = [
{:cola => "hello", :colb => "bye"},
{:colb => "bye", :cola => "hello"}
]
producing the following table using the code from the majority (including best answer) of the answers on this thread:
cola | colb
-------------
hello | bye
-------------
bye | hello
You should do this instead:
require "csv"
csv_rows = [
{:cola => "hello", :colb => "bye"},
{:colb => "bye", :cola => "hello"}
]
column_names = csv_rows.first.keys
s=CSV.generate do |csv|
csv << column_names
csv_rows.each do |row|
csv << column_names.map{|column_name| row[column_name]} #To be explicit
end
end
Try this:
require 'csv'
data = { 'one' => '1', 'two' => '2', 'three' => '3' }
CSV.open("data.csv", "a+") do |csv|
csv << data.keys
csv << data.values
end
Lets we have a hash,
hash_1 = {1=>{:rev=>400, :d_odr=>3}, 2=>{:rev=>4003, :d_price=>300}}
The above hash_1 having keys as some id 1,2,.. and values to those are again hash with some keys as (:rev, :d_odr, :d_price).
Suppose we want a CSV file with headers,
headers = ['Designer_id','Revenue','Discount_price','Impression','Designer ODR']
Then make a new array for each value of hash_1 and insert it in CSV file,
CSV.open("design_performance_data_temp.csv", "w") do |csv|
csv << headers
csv_data = []
result.each do |design_data|
csv_data << design_data.first
csv_data << design_data.second[:rev] || 0
csv_data << design_data.second[:d_price] || 0
csv_data << design_data.second[:imp] || 0
csv_data << design_data.second[:d_odr] || 0
csv << csv_data
csv_data = []
end
end
Now you are having design_performance_data_temp.csv file saved in your corresponding directory.
Above code can further be optimized.
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
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')
I have the following code to export to excel
on my controller:
def users_report
#users = Kid.where(confirmation_token: nil).paginate(:page => params[:page], :per_page => 30)
#userxls = Kid.where(confirmation_token: nil)
respond_to do |format|
format.html
format.xls { send_data #userxls.to_csv({col_sep: "\t"}) }
end
end
on my model:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << ["Name", "Surname", "E-mail", "Birthday", "School", "Class", "Native Language", "Practised Language", "Conversations same Native", "Convserations different Native", "Message same Native", "Message different Native", "Posts", "Videos watched", "Clossed/Finished calls", "Missed calls", "Connections per Week", "Nb of foreign friends", "Nb of friends same country", "Activation Date", "Email Parent", "Parent Activated"]
all.each do |kid|
kids = Array.new
kid.name = kid.name rescue "No name"
kid.surname = kid.surname rescue "No surname"
kid.email = kid.email rescue "No email"
kid.birthday = kid.birthday rescue "No age"
kid.school.name = kid.school.name rescue "No School"
kid.courses.first.name = kid.courses.first.name rescue "No Course"
kid.native_languages = kid.native_languages rescue "No native language"
kid.practice_languages = kid.practice_languages rescue "No practise language"
kid.number_of_native_conversations = kid.number_of_native_conversations rescue "0"
kid.number_of_foreign_conversations = kid.number_of_foreign_conversations rescue "0"
kid.number_of_native_messages = kid.number_of_native_messages rescue "0"
kid.number_of_foreign_messages = kid.number_of_foreign_messages rescue "0"
kid.number_of_activity_posts = kid.number_of_activity_posts rescue "0"
kid.number_of_finished_closed_calls = kid.number_of_finished_closed_calls rescue "0"
kid.number_of_missed_calls = kid.number_of_missed_calls rescue "0"
kid.avg_of_connections_week = kid.avg_of_connections_week rescue "0"
kid.number_of_foreign_friends = kid.number_of_foreign_friends rescue "0"
kid.number_of_friends_same_country = kid.number_of_friends_same_country rescue "0"
kid.confirmed_at = kid.confirmed_at.try(:strftime, "%d/%m/%Y")
kid.tutor.email = kid.tutor.email rescue 'No parent email'
kid.tutor.confirmed = kid.tutor.confirmed rescue 'No parent email'
kids << [kid.name, kid.surname, kid.email, kid.birthday, kid.school.name, kid.courses.first.name, kid.native_languages, kid.practice_languages, kid.number_of_native_conversations, kid.number_of_foreign_conversations, kid.number_of_native_messages, kid.number_of_foreign_messages, kid.number_of_activity_posts, kid.number_of_finished_closed_calls, kid.number_of_missed_calls, kid.avg_of_connections_week, kid.number_of_foreign_friends, kid.number_of_friends_same_country, kid.confirmed_at.try(:strftime, "%d/%m/%Y"), kid.tutor.email, kid.tutor.confirmed? ]
kids.each do |k|
csv << k
end
end
end
end
I know this code is horrible, but I getting the following problem:
1) I can insert electments directo on my csv array, because when I open the file I get something like #kid<3248293028>, instead of name, surname, email, bla bla bla, this I why I have to insert first on a kids array and then using a each, insert in the csv array.
2)Inside my model I have several methods, like age, to get the age of the kids:
def age
now = Time.now.utc.to_date
(now.year - self.birthday.year - (self.birthday.to_date.change(:year => now.year) > now ? 1 : 0)) rescue (now.year - self.birthday.year)
end
but the t0_csv method does not see this method, I have the same problem with other methods like:
sentence_native_languages_of
sentence_practise_languages_of
number_of_native_conversations
number_of_foreign_conversations
number_of_native_messages
etc etc etc, all of then exist in the model but the to_csv never see this methods.
Why, help me to understand.
Thanks in advance.
UPDATE
here is one of the method that my to_csv method can't see
def number_of_native_conversations
na_messages = 0
KidConversation.all_for(self).each do |conversation|
na_messages += 1 unless conversation.foreign_conversation_for? self
end
return na_messages
end
Class UserController
def export_users
users = User.all
stream_csv do |csv|
csv << ["Name","Email","Gender"]
users.each do |i|
csv << [i.name,i.email,i.gender]
end
end
end
def stream_csv
require 'fastercsv'
filename = params[:action] + ".csv"
#this is required if you want this to work with IE
if request.env['HTTP_USER_AGENT'] =~ /msie/i
headers['Pragma'] = 'public'
headers["Content-type"] = "text/plain"
headers['Cache-Control'] = 'no-cache, must-revalidate, post-check=0, pre-check=0'
headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
headers['Expires'] = "0"
else
headers["Content-Type"] ||= 'text/csv'
headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
controller.response.headers["Content-Transfer-Encoding"] = "binary"
end
render :text => Proc.new { |response, output|
csv = FasterCSV.new(output, :row_sep => "\r\n")
yield csv
}
end
end
Err: "#Proc:0x9382539#/sites/app/controllers/export_controller.rb:56"
Using Ruby 1.8 and Rails 3.0.9
So I think the problem here is that I'm not using "Proc" right. Or it's not supposed to act like just another block...
I thought about programming a new logic into the class so that reads better. But if somebody could explain to me why my code is wrong or at least point me in a new direction than I might be able to learn something new here. Thanks
Note: Found a better way:
def export_inverts
require 'fastercsv'
inverts = Invert.all
filename = params[:action] + ".csv"
#this is required if you want this to work with IE
if request.env['HTTP_USER_AGENT'] =~ /msie/i
headers['Pragma'] = 'public'
headers["Content-type"] = "text/plain"
headers['Cache-Control'] = 'no-cache, must-revalidate, post-check=0, pre-check=0'
headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
headers['Expires'] = "0"
else
headers["Content-Type"] ||= 'text/csv'
headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
headers["Content-Transfer-Encoding"] = "binary"
end
csv_string = FasterCSV.generate do |csv|
csv << ["Genus","Species","Common Name","Pet Name","Gender"]
inverts.each do |i|
csv << [i.scientific_name,i.scientific_name,i.common_name,i.pet_name,i.gender]
end
end
render :text => csv_string
end
Yield can only be used inside a function or a block. Yield is used in a function that takes a block to say, yield some value into the block. Actually it says yield this value into the proc that the block has been converted into with the ampersand operator (in most cases). However, you could pass a Proc to a function that was expecting it.
Here, you just want to return the value from the proc and "yield" isn't needed.