Rails - How to export nested resources to CSV in Rails? - ruby-on-rails

I am trying to export some data to CSV in Rails 4. I have two models: Excursions and Inscriptions. One excursion has many inscriptions and one inscription belongs to one excursion.
I have my nested routes defined this way:
resources :excursions do
resources :inscriptions
get 'exportcsv' => 'excursions#download'
end
So the behavior I am trying to achieve is: when I visit the route /excursions/1/exportcsv, a CSV will be downloaded to my computer and it will contain all the inscriptions with excursion_id = 1 in CSV format.
In my excursion model I have defined self.to_csv:
def self.to_csv(options = {})
CSV.generate(options) do |csv|
csv << column_names
self.inscriptions.each do |inscription|
csv << inscription.attributes.values_at(*column_names)
end
end
end
And my excursion controller's method download:
def download
#excursion = Excursion.find(params[:id])
respond_to do |format|
format.html
format.csv { send_data #excursion.to_csv }
end
end
EDIT: When I go to a route like: /excursions/:id/exportcsv the server is throwing an ActiveRecord::RecordNotFound error. This error is easy to solve, but if I solve the RecordNotFound I get an ActionController::UnknownFormat in this line:
def download
#excursion = Excursion.find(params[:id])
respond_to do |format| ########THIS LINE
format.html
format.csv { send_data #excursion.to_csv }
end
end
What I am doing wrong? Maybe all this approach is not correct...

I would update the routes to the following:
resources :excursions do
get 'download', on: :member, constraints: { format: /(html|csv)/ }
resources :inscriptions
end
Also there is a bug in your model code. You are exporting inscriptions but are using Excursion column_names instead of Inscription column_names. Following is the updated model code.
class Excursion < ActiveRecord::Base
def to_csv(options = {})
CSV.generate(options) do |csv|
inscription_column_names = Inscription.column_names
csv << inscription_column_names
self.inscriptions.each do |inscription|
csv << inscription.attributes.values_at(*inscription_column_names)
end
end
end
Now try to access http://localhost:3000/excursions/:id/download.csv
Replace :id with an existing Excursion records id. If you still encounter ActiveRecord::RecordNotFound error, then the problem could be that you are trying to access an Excursion that doesn't actually exist in the database.

Related

Rails' export csv function "undefined method `export' for nil:NilClass"

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)) %>

Undefined method for array csv export

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.

Rails: Export CSV table based on URL parameters

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)

ActiveAdmin: How to have a specific set of resources while downloading them all in CSV format?

AcitveAdmin provides a link at the bottom of the index page to download all resources in multiple formats, and one of the formats is CSV. It is gonna take all the resources, put them in a CSV file, and give that back to us. In my situation, I have a condition that there are certain resources that can be downloaded via CSV, like:
User.downloadables
But I'm unable to figure it out how to give this set instead of User.all to CSV format?
I have looked into documentation, and it doesn't say much except changing the layout of a CSV file.
Apparently, I couldn't find a way to pass a specific set of records to CSV file, so I wrote the custom controller method for it in ActiveAdmin, and that's how I accomplished it:
app/models/user.rb:
scope :downloadables, -> { where(status: :downloadable) }
def self.to_csv
downloadable_users = User.downloadables
CSV.generate do |csv|
csv << column_names
downloadable_users.each do |user|
csv << user.attributes.values_at(*column_names)
end
end
end
app/admin/user.rb:
First, I generated a collection action that will let us download CSV file:
collection_action :download_csv do
redirect_to action: :download_csv
end
Now, to have a link on index page through which one would be able to download the records in CSV file:
action_item only: :index do
link_to "Download CSV", download_csv_admin_users_path
end
And finally to have a controller method that will actually call the method from model, and do the rest of the magic:
controller do
def download_csv
respond_to do |format|
format.html { send_data User.to_csv, filename: "users-#{Data.today}.csv" }
end
end
end
And that's it. It is working.
You can also customize the csv export as well for active admin.
ActiveAdmin.register Post do
csv force_quotes: true, col_sep: ';', column_names: false do
column :title
column(:author) { |post| post.author.full_name }
end
end
Cheers

CSV downloading the object and not actual values Rails

I'm not quite sure what I am overlooking when attempting to allow csv download on my Game model and I'm getting a little lost.
On the profile show page I render an index like list of games associated with that user, i.e. their game schedule.
The Profiles Controller-
def show
#user = User.find_by_profile_name(params[:id])
if #user
#listings = #user.listings
#games = #user.games
respond_to do |format|
format.html
format.csv {send_data #games.to_csv}
end
return
render action: :show
else
render file: "public/404", status: 404, formats: [:html]
end
end
Then in the game.rb I define the method to_csv
def self.to_csv
CSV.generate do |csv|
csv << column_names
all.each do |item|
csv << item.attributes.values_at(*column_name)
end
end
end
And on the profile show page to download the expected csv game schedule
<%= link_to "Download my Schedule", profile_path(format: 'csv')%>
I believe this might be my issue lies, but that doesn't quite explain what I get in my csv which is just a game object
file-
Here is my routes.rb
resources :games
match 'friendships/:friend_id' => 'user_friendships#new', :as => :new_friendship
match 'dashboard' => 'dashboard#show', :as => :dashboard
root to: "profiles#index"
get '/players', to: 'profiles#index', as:'players'
get '/players', to: 'profiles#index', as:'users'
get '/:id', to: "profiles#show", as: 'profile'
The file should be formatted with the column names (location, opponent, time, etc) as the header line and the corresponding lines with their respective values for each instance associated to a user.
I think the to_csv method inside game should be re-declared as -
passed in the games array which needed to be converted.
the param passed to values_at is column_names not column_name.
def self.to_csv(games)
CSV.generate do |csv|
csv << column_names
games.each do |item|
csv << item.attributes.values_at(*column_names)
end
end
end
and in the controller, the code should be:
def show
#user = User.find_by_profile_name(params[:id])
if #user
#listings = #user.listings
#games = #user.games
respond_to do |format|
format.html
format.csv {send_data Game.to_csv(#games)}
end
return
render action: :show
else
render file: "public/404", status: 404, formats: [:html]
end
end
otherwise, you will output all the games no matter which user you are using.
Although cenjongh's answer isn't wrong, let me elaborate on it.
The syntax Game.to_csv(#games) goes against Ruby's/Rails' object oriented approach for me.
Since the CSV generation code in your case is totally model independent (you don't make any assumptions of column names, etc.) you could feed this method any array of models, i.e. Game.to_csv(#shampoos) which would still work but wouldn't read very well.
Since Rails scopes the all method according to the criteria attached to the ActiveRelation object, using it in your class method wouldn't result in an output of all the games.
Assuming you're using at least Rails 3.0 the line #games = #user.games would give you an ActiveRelation object, not an array, meaning you can call #games.to_csv (or to make it even clearer #user.games.to_csv) directly, which reads what it is, namely converting a list of games, that belong to a user into CSV.
Oh, and I guess this is just testing code, but the return shouldn't be there. And the render statement should go into the block of format.html.

Resources