I am trying to generate a CSV output with data from my database. I would like to provide these data to a third party, so I picture I would give to someone a URL (website.com/api_data/cars) and by accessing this URL the person would be able to work with it - I think I want to access the URL and then to see there (in the action) data displayed and separated by , or ;.
But how to do that?
So far, I am trying following approach:
csv_string = CSV.generate do |csv|
cols = ["column one", "column two", "column three"]
csv << cols
csv << ["A", "B", "C"]
#filename = "data-#{Time.now.to_date.to_s}.csv"
end
send_data(csv_string, :type => 'text/csv; charset=utf-8; header=present', :filename => #filename)
This is in the controller generate_data and action csv_cars.
When I run this action (webste.com/generate_data/csv_cars), it will automatically pop up a window to download the file.
But how to write the CSV content to the action? So when I open the URL, I'll see there written the content from the database?
I know this is an old thread but I came across it in my search so in case someone else does the same, here's my answer and what worked for me.
I think bhanu had a good way of going about it but I did change something. Instead of doing #cars within the respond_to, I just called send_data Cars.to_csv since, as Rob stated, it was made as a class method. It worked beautifully for me.
class Car < ActiveRecord::Base
def self.to_csv(make)
attributes = %w{id name price} #customize columns here
cars = Car.where(maker_name: make)
CSV.generate(headers: true) do |csv|
csv << attributes
cars.each do |car|
csv << attributes.map{ |attr| car.send(attr) }
end
end
end
end
And then in the controller
class CarsController < ApplicationController
def index
send_data Cars.to_csv('Chevy'), filename: "cars-#{Date.today}.csv"
end
end
I understand that this will be called when you go to cars/index but you can put that into any method, if statement or anything you want and just have it called whenever you would like from there. You can also have arguments, as I did above with make, and query for certain fields. It was definitely a lot easier than I thought it was going to be. Hope this helped someone.
You need to do something like this.
def csv_cars
headers = ['column one', 'column two', 'column three']
csv_data = CSV.generate(headers: true) do |csv|
csv << headers
csv << ["A", "B", "C"]
end
send_data csv_data, filename: "data-#{Date.today.to_s}.csv", disposition: :attachment
end
define a to_csv method in your model as shown below
class Car < ActiveRecord::Base
def self.to_csv
attributes = %w{id name price} #customize columns here
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |car|
csv << attributes.map{ |attr| car.send(attr) }
end
end
end
end
Later in your controller
class CarsController < ApplicationController
def index
#cars = Car.all
respond_to do |format|
format.html
format.csv { send_data #cars.to_csv, filename: "cars-#{Date.today}.csv" }
end
end
end
Related
I'm making an export to csv file functionality in a Ruby on Rails repo and I'm almost done. However, when I press the "Export all" button, I get the undefined method `export' for nil:NilClass error. The log shows that format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" } went wrong. What am I missing please?
This is model
class Foo < ApplicationRecord
has_many :bars
def export
[id, name, foos.map(&:name).join(' ')]
end
end
This is part of controller
def index
#foos = Foo.all
end
def export
all = Foo.all
attributes = %w{name}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |foo|
csv << attributes.map{ |attr| foo.send(attr) }
end
respond_to do |format|
format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" }
end
end
end
def name
"#{foo_id} #{name}"
end
This is View
<button class="btn btn-success">export all</button>
This is Routes
Rails.application.routes.draw do
resources :foos
get :export, controller: :foos
root "foos#index"
end
This is Rake (lib/tasks/export.rb)
namespace :export do
task foo: :environment do
file_name = 'exported_foo.csv'
csv_data = Foo.to_csv
File.write(file_name, csv_data)
end
end
Start by creating a service object that takes a collection of records and returns CSV so that you can test the CSV generation in isolation:
# app/services/foo_export_service.rb
# Just a Plain Old Ruby Object that converts a collection of foos into CSV
class FooExportService
# The initializer gives us a good place to setup our service
# #param [Enumerable] foo - an array or collection of records
def initialize(foos)
#headers = %w{name} # the attributes you want to use
#foos = foos
end
# performs the actual work
# #return [String]
def perform
CSV.generate do |csv|
#foos.each do |foo|
csv << foo.serializable_hash.slice(#headers).values
end
end
end
# A convenient factory method which makes stubbing the
# service easier
# #param [Enumerable] foos - an array or collection of records
# #return [String]
def self.perform(foos)
new(foos).perform
end
end
# example usage
FooExportService.perform(Foo.all)
Not everything in a Rails application needs to be jammed into a model, view or controller. They already have enough responsiblities. This also lets you resuse the code for example in your rake task if you actually need it.
This simply iterates over the collection and uses Rails built in serialization features to turn the model instances into hashes that can be serialized as CSV. It also uses the fact that Hash#slice also reorders the hash keys.
In your controller you then just use the service object:
class FoosController
def export
#foos = Foo.all
respond_to do |format|
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
You don't even really need a separate export action in the first place. Just use MimeResponds to add CSV as an availble response format to the index:
class FoosController
def index
# GET /foos
# GET /foos.csv
#foos = Foo.all
respond_to do |format|
format.html
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
<%= link_to("Export as CSV", foos_path(format: :csv)) %>
I'm getting the following error when trying to export to a csv:
undefined method `dd_export' for #<Array:0x00007fc4836f1798>
I think it's the relationship between the model and controller, but can't work out why this is. I'm using a custom dd_export convention as more csv download may be added, and also using to_csv doesn't seem to export what's required in my model.
day_degree_export.rb
class DayDegreeExport < ApplicationRecord
def self.dd_export
attributes = %w{Date Min_Temp Max_Temp}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |dd|
csv << [
dd['date'],
dd['variables'][1]['value']
dd['variables'][0]['value']
]
end
end
end
end
end
day_degree_export_controller.rb
class DayDegreeExportController < ApplicationController
dd_results
data = HTTParty.get("url_link")
#ddexport = JSON.parse(data.body)
respond_to do |format|
format.html
format.csv { send_data #ddexport['data'].dd_export, filename: "ddexport.csv" }
end
end
end
I have required csv in my application.rb.
If you want to call #ddexport['data'].dd_export, you'd have to patch the Array class for dd_export, but a better way to implement it is to save your array as DayDegreeExport records.
class DayDegreeExportController < ApplicationController
def dd_results
data = HTTParty.get("url_link")
#ddexport = JSON.parse(data.body)
DayDegreeExport.create(#ddexport) # you can do this if your #ddexport is array of hashes with matching keys
respond_to do |format|
format.html
format.csv { send_data DayDegreeExport.dd_export, filename: "ddexport.csv" }
end
end
end
Since you are not intending to store #ddexport['data'], then there is no need for DayDegreeExport to inherit from ApplicationRecord. Instead, I would suggest you do something like:
# app/services/day_degree_export_service.rb
class DayDegreeExportService
CSV_ATTRIBUTES = %w(
Date
Min_Temp
Max_Temp
).freeze
attr_accessor *%w(
args
).freeze
class << self
def call(args={})
new(args).call
end
end
def initialize(args)
#args = args
end
def call
CSV.generate(headers: true) do |csv|
csv << CSV_ATTRIBUTES
dd_export_data.each do |dd|
csv << [
dd['date'],
dd['variables'][1]['value']
dd['variables'][0]['value']
]
end
end
end
private
def dd_export_data
#dd_export_data ||= JSON.parse(HTTParty.get("url_link"))['data']
end
end
Which you would use something like:
class DayDegreeExportController < ApplicationController
def dd_results
respond_to do |format|
format.html
format.csv { send_data DayDegreeExportService.call, filename: "ddexport.csv" }
end
end
end
Now, a few comments:
1) I never use CSV so I have no idea whether the code is going to work. I took a look at the docs and I think this may be close, but you may have to fiddle with it.
2) A PORO (plain old ruby object) gets the job done here. You can see I suggest you place it in a services directory and rails will pick it up automatically.
3) In the class-level call method, I include optional args. That's just for example purposes. Some other time you might want to pass in arguments.
4) For services, I like to use the call method instead of service-specific methods like dd_results. It's just a me thing. But, then, you don't have to think about what to call your methods in your service. As long as your services and their purpose have a 1:1 relationship, then the class name describes what the service does instead of the method name.
5) Using class << self creates class methods. It's basically the same as doing def self.call.
6) This all assumes, I supposed, that JSON.parse(HTTParty.get("url_link"))['data'] returns an array of data and that each datum has the keys that you indicate.
I try to export a CSV table based on parameters in URL in Rails 4.
The URL parameters look like this "classes?date=2018-08-01" and depending on the URL date the view changes a displayed table with data for given month. I want to export these data. However, the Export button downloads always only data for the current month. What am I doing wrong?
I added a simplified code, not the exact copy. So if you need to see anything more from the code, write a comment.
Controller:
def index
#date = if params[:date].present?
Time.zone.parse(params[:date])
else
Time.zone.now
#classes = Class.includes(:teacher).on_month(#date.to_date)
respond_to do |format|
format.html
format.csv { send_data #classes.to_csv }
end
end
View:
= link_to 'Export', classes_path(#classes, format: :csv)
Model:
def self.to_csv
column_names = %w{ Teacher Class }
CSV.generate(headers: true) do |csv|
csv << column_names
all.each do |class|
csv << [class.teacher.name, class.class_name]
end
end
end
In your link to export you should include the date instead of the classes since in your controller if params[:date] is missing then you are using the current date.
= link_to 'Export', classes_path(date: #date, format: :csv)
Having followed the RailsCast on importing CSV (http://railscasts.com/episodes/396-importing-csv-and-excel), I am trying to validate that the file being uploaded is a CSV file.
I have used the gem csv_validator to do so, as documented here https://github.com/mattfordham/csv_validator
And so my model looks like this:
class Contact < ActiveRecord::Base
belongs_to :user
attr_accessor :my_csv_file
validates :my_csv_file, :csv => true
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << column_names
all.each do |contact|
csv << contact.attributes.values_at(*column_names)
end
end
end
def self.import(file, user)
allowed_attributes = ["firstname","surname","email","user_id","created_at","updated_at", "title"]
CSV.foreach(file.path, headers: true) do |row|
contact = find_by_email_and_user_id(row["email"], user) || new
contact.user_id = user
contact.attributes = row.to_hash.select { |k,v| allowed_attributes.include? k }
contact.save!
end
end
end
But my system still allows me to select to import non-CSV files (such as .xls), and I receive the resulting error: invalid byte sequence in UTF-8.
Can someone please tell me why and how to resolve this?
Please note that I am using Rails 4.2.6
You can create a new class, let's say ContactCsvRowValidator:
class ContactCsvRowValidator
def initialize(row)
#row = row.with_indifferent_access # allows you to use either row[:a] and row['a']
#errors = []
end
def validate_fields
if #row['firstname'].blank?
#errors << 'Firstname cannot be empty'
end
# etc.
end
def errors
#errors.join('. ')
end
end
And then use it like this:
# contact.rb
def self.import(file, user)
allowed_attributes = ["firstname","surname","email","user_id","created_at","updated_at", "title"]
if file.path.split('.').last.to_s.downcase != 'csv'
some_method_which_handle_the_fact_the_file_is_not_csv!
end
CSV.foreach(file.path, headers: true) do |row|
row_validator = ContactCsvRowValidator.new(row)
errors = row_validator.errors
if errors.present?
some_method_to_handle_invaid_row!(row)
return
end
# other logic
end
end
This pattern can easily be modified to fit your needs. For example, if you need to have import on several different models, you could create a base CsvRowValidator to provide basic methods such as validate_fields, initialize and errors. Then, you could create a class inheriting from this CsvRowValidator for each model you want, having its own validations implemented.
I have in my controller a section that generates an array of Song objects. For each song object, I want to output some of it's information to a csv file. I found instructions online and followed them to the best of my ability.
The controller:
def index
#stations = Station.all
#csv crap
#played = []
Station.first.users.each do |u|
u.playlists.where("updated_at > ?", Date.today).each do |p|
#played << p
end
end
#songs = []
if !#played.empty?
#played.each do |pl|
pl.songs.each do |s|
#songs << s
end
end
end
#data = []
#data << "Song"
#data << "Album"
#data << "Artist"
(0...#songs.length).each do |s|
son = #songs[s]
#data << [son.title, Album.find(son.album_id).name, Artist.find(son.artist_id).name]
end
respond_to do |format|
puts "******************\n\n\n\n\n\n\n\n"
puts #songs.inspect
puts "******************\n\n\n\n\n\n\n\n"
format.html
format.csv { send_data #data.to_csv(), filename:"daily_report.csv" }
end
end
The model:
def self.to_csv(options = {})
puts "fuck all"
CSV.generate(options) do |csv|
csv << column_names
options.each do |item|
csv << item.title
csv << item.attributes.values_at(*column_names)
csv << item.attributes.values_at(*column_names)
end
end
end
For some reason, the controller is never using the model to generate the csv, which it should be. I have to extra stuff in the controller to try to make it generate correctly there, but I really want it to generate in the model. Any idea why it never hits the self.to_csv in the model?
Your model's to_csv method isn't being called because you're not calling it - you're calling a to_csv instance method on an array.
You need something along the lines of
ModelClass.to_csv(#songs)
and your to_csv method should be closer to
def self.to_csv(data, options = {})
CSV.generate(options) do |csv|
csv << column_names
data.each do |item|
csv << [item.title]
csv << item.attributes.values_at(*column_names)
csv << item.attributes.values_at(*column_names)
end
end
end
I'm a bit confused about what you're adding to the csv (for example why are the item attributes added twice?) but this should at least produce a csv.