DSL for rendering and streaming CSVs in Rails apps
# app/controllers/users_controller.rbclassUsersController < ApplicationControllerincludeActionController::Live# required to stream; decisive will fall back to rendering without itdefindex@users=User.allendend# app/views/users/index.csv.decisivecsv@users,filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv"docolumn"Email"# omitted accessor field gets inferred: user.emailcolumn"Full name",:name# explicit accessor field: user.namecolumn"Signed up"do |user| # accepts a block for doing something specialuser.created_at.to_dateendendThen visit /users.csv to stream a file named "users-2010_01_01.csv" with the following contents:
| Full name | Signed up | |
|---|---|---|
| frodo@example.com | Frodo Baggins | 2002-06-19 |
| sam@example.com | Samwise Gamgee | 2008-10-13 |
Sometimes, we don't know exactly what the headers will be until we've iterated through every record.
For example, lets say that the Frodo record has a #faqs attribute of { "Riddles?" => "Yes" }, while Sam's is { "Hero?" => "Frodo" }.
In this case, you can pass stream: false to #csv, and the method will yield each record to the block:
# app/views/users/index.csv.decisivecsv@users,filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv",stream: falsedo |user|
column"Email"column"Full name"user.faqs.favorite_questions_and_answers.eachdo |question,answer|
columnquestion,answerendcolumn"Signed up",user.created_at.to_date# we have access to the user record directlyendVisiting /users.csv will render a file named "users-2010_01_01.csv" with the following contents:
| Full name | Riddles? | Hero? | Signed up | |
|---|---|---|---|---|
| frodo@example.com | Frodo Baggins | Yes | 2002-06-19 | |
| sam@example.com | Samwise Gamgee | Frodo | 2008-10-13 |
Decisive evaluates the blocks in your template against its own objects, but they still reach the view's helper methods, just like the rest of the template:
# app/views/users/index.csv.decisivecsv@users,filename: "users.csv",stream: falsedocolumn"Signed up"do |user|
l(user.created_at,format: :short)# a view helperendendThe one thing blocks cannot see is the view's instance variables: @users above
is read when the template runs, but inside a block @users is nil. Assign it to
a local first, or reach for a helper method.
Errors in your decisive template will often be swallowed while streaming is enabled, resulting in only some of the csv being rendered, without any explanation. You can temporarily switch decisive into non-streaming mode to see these errors:
# decisive templatecsv@records,filename: "report.csv",stream: falsedo
...
end# controllerclassReportController < ApplicationController# include ActionController::Live
...
endSometimes the streaming templates will hang in test mode. This could be because action_controller/test_case has been loaded, which monkeypatches AC::Live in a way that seems to breaks things. If you are running into this, try this workaround (cucumber example):
# features/support/fix_ac_live.rb# requiring cucumber/rails requires rails/test_help which requires action_controller/test_case which redefines AC::Live#new_controller_thread to be single threaded, which breaks our downloads# set it back to original method definitionmoduleActionControllermoduleLivesilence_redefinition_of_method:new_controller_threaddefnew_controller_thread# :nodoc:Thread.new{t2=Thread.currentt2.abort_on_exception=trueyield}endendendAfter checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Bug reports and pull requests are welcome on GitHub at https://github.com/botandrose/decisive.
The gem is available as open source under the terms of the MIT License.