I'm working on a project in rails and recently watched this great gorails.com episode on exporting data as CSV (https://gorails.com/episodes/export-to-csv). I can get it to work, but I really want to get the absolute url of each of the "tasks" so in the CSV export it would show in each row as a 4th column the link to the "task" eg: "localhost:3000/tasks/1" "localhost:3000/tasks/2" etc. Something like "task_url" as the column header
I haven't found any resource online that helps with this. Any thoughts on how to do this in the most efficient way? Thank you for the help! Here is the working code:
def self.to_csv
attributes = %w{id task_name task_description}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |task|
csv << attributes.map{ |attr| task.send(attr) }
end
end
end
And I would like to do something like this (emphasis added around task_url):
def self.to_csv
#need to declare what task_url is here
attributes = %w{id task_name task_description **task_url**}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |task|
csv << attributes.map{ |attr| task.send(attr) }
end
end
end
You could update your Task model with a #task_url instance method so that calling send on the task instances works. Then next implement the #task_url method with the necessary logic to the build and return the appropriate link for the individual task object. Something like:
class Task
include Rails.application.routes.url_helpers
...
def self.to_csv
attributes = %w{id task_name task_description task_link}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |task|
csv << attributes.map{ |attr| task.send(attr) }
end
end
end
def task_link
task_url(self)
end
end
In this example, the links are constructed using the rails routes helpers. And to access them, we'll need to the include the #url_helpers module since they are not available (by default) in the model layer.
Notice that in the example, the method is called #task_link; the assumption being that the route helper method is already named #task_url and so we avoid overriding it.
I have this class method in my model:
def self.to_csv
attributes = %w(title)
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |campaign|
csv << campaign.attributes.values_at(*attributes)
end
end
end
I'm looking for good ways to test this method with Rspec. Does anyone have good techniques for this method?
I have a few comments:
I would not use all unless you are in a background job or you know the collection would not be that big
if you really have to use all, then dont use .each use .find_each which would do queries in batches
use factory bot if you can
For the spec itself, I would do:
it "creates expected csv" do
allow(described_class).to receive(:all).and_return([
described_class.new(title: "title1"),
described_class.new(title: "title2")
])
expect(described_class.to_csv).to eq "title\ntitle1\ntitle2\n"
end
I have successfully gotten CSV files to be imported following the steps outlined in the Railscast 396 episode. However, now I'm trying to import a csv that is nested. I have seen other posts where people are trying to import both the parent and the child data but in this case I'm trying to only import the child data on the econ_report show view.
For example, I have econ_results that I'm trying to import that is nested under an econ_report. The rake file shows the following link.
import_econ_report_econ_results
When I try to load the show page for the econ_report I get the following error
No route matches {:action=>"import", :controller=>"econ_results", :id=>"1"} missing required keys: [:econ_report_id]
I have the following code in the econ_result.rd file:
def self.to_csv
CSV.generate do |csv|
csv << column_names
all.each do |sub|
csv << sub.attributes.values_at(*column_names)
end
end
end
def self.import(file)
CSV.foreach(file.path,headers: true) do |row|
EconResult.create! row.to_hash
end
end
Lastly, in the econ_results_controller I have the following code:
before_action :set_econ_report
def set_econ_report
#econ_report = EconReport.find(params[:econ_report_id])
end
def import
EconResult.import(params[:file])
end
def create
#econ_result = #econ_report.econ_results.create(econ_result_params)
redirect_to #econ_report
end
Is the designation of #econ_report causing the conflict in the controller?
You need to tell rails that the record you are passing is for the econ_report_id param. The default is that the route helper will use a passed record for the id param.
<%= link_to import_econ_report_econ_results_path(econ_report_id: #econ_report) %>
I would write a helper method since the name is ridiculously long.
I'm saving records into CSV using after_save_filer. I don't receive any error and also I can't find file, what was generated(if it exists).
Here is code what I'm using:
after_save :to_csv
def to_csv(options = {})
require 'csv'
CSV.open("C:/project/myfile.csv", "w") do |csv|
csv << self.class.column_names
csv << self.attributes.values_at(*column_names)
end
end
How can I check is after save filter has been run ? How I can set the path ?
To check if the after_save was run you can either put a debugger statement inside the to_csv method and see if it stops there when you save the model, or you could simply put a puts "I am in to_csv method inside the method and look for it in the console.
Currently, if I want to apply a method to a group of ActiveRecord objects, I have to structure the call like so:
messages = Message.find(:all)
csv = Message.to_csv(messages)
How can I define the method so it's structured like so?
messages = Message.find(:all)
csv = messages.to_csv
This is the current model code:
require 'fastercsv'
class Message < ActiveRecord::Base
def Message.to_csv(messages)
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
for m in messages
csv << [m.from,m.to,m.created_at]
end
end
end
end
The following will call to_csv on all instances included in the messages array.
messages = Message.find(:all)
csv = messages.map { |message| message.to_csv }
In Rails, in Ruby 1.9 or with Symbol#to_proc available through other means, you can also shorten it to:
csv = messages.map(&:to_csv)
The longer form is useful when you want to make a more complex operation:
csv = messages.map { |message|
if message.length < 1000
message.to_csv
else
"Too long"
end
}
Put this in a file in lib/. I would recommend calling it something like base_ext.rb
require 'fastercsv'
class ActiveRecord::Base
def self.to_csv(objects, skip_attributes=[])
FasterCSV.generate do |csv|
csv << attribute_names - skip_attributes
objects.each do |object|
csv << (attribute_names - skip_attributes).map { |a| "'#{object.attributes[a]}'" }.join(", ")
end
end
end
end
After that, go to config/environment.rb and put require 'base_ext' at the bottom of the file to include the new extension. Upon restarting your server you should have access to a to_csv method on all model classes and when you pass it an array of objects should generate a nice CSV format for you.
FasterCSV patches the Array class and adds a 'to_csv' method to it already, but it doesn't do what you want. You could overwrite it yourself by doing something like:
class Array
def to_csv(options = Hash.new)
collect { |item| item.to_csv }.join "\n"
end
end
Or something along those lines, but that's kind of crappy.
Honestly, it makes more sense the way you've done it as a class method on your model.
You could create a method on your Message class to do something along the lines of...
In your controller....
#csv_file = Message.send_all_to_csv
In your model...
require 'fastercsv'
class Message < ActiveRecord::Base
def send_all_to_csv
#messages = Find.all
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
for message in #messages
csv << [message.from,message.to,message.created_at]
end
end
# do something with your csv object (return it to the controller
# or pass it on to another class method
end
end
You could define the method directly on the messages object itself, but if you do that, the method would only be available to that specific instance:
def messages.to_csv()
FasterCSV.generate do |csv|
csv << ["from", "to", "received"]
self.each { |m| csv << [m.from, m.to, m.created_at] }
end
end
Then you could call it like so:
messages.to_csv
I'm a Ruby newbie, so I'm not sure if this is idiomatic Ruby or not: that is, I'm not sure how common or accepted it is to define new methods directly on object instances, I only know it's possible ;-)
If it is isolated to one AR model I would add a to_custom_csv_array instance method
def to_custom_csv_array
[self.from,self.to,self.created_at]
end
then override find on the class
def self.find(*args)
collection = super
collection.extend(CustomToCSV) if collection.is_a?(Array)
end
and in CustomToCSV define the to_custom_csv to generate the csv
module CustomToCSV
def to_custom_csv
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
csv << self.map {|obj| obj.to_custom_csv_array}
end
end
end
This is from memory but should work.
I know that it is a very old question but just thought to provide a feedback anyway.
check blog http://blog.zahiduzzaman.com/2013/07/adding-tocsv-method-to-active-record.html
just another way of achieving this