Last Updated: February 22, 2017
·
9.197K
· mroach_

Rails: Exporting data to spreadsheets

Allowing admins of data systems to export data to spreadsheets allows them to manipulate it, make it pretty, generate reports, and pass it on in their own way without the developer having to bake in all that functionality to the app. Out of the box Rails can dump to XML and JSON, but that's not so user friendly. Dumping to CSV is closer, but it's too easy to have issues with field termination and value quoting, plus you have no chance to style the output. A little can go a long way.

Enter the axlsx gem. It allows you to quickly and easily create spreadsheets in Rails, exporting to the .xlsx format which is readable by Excel, OpenOffice, and Google Docs. Solid coverage. When you combine this with the axlsx-rails gem you can generate spreadsheets as easily as you generate HTML with an ERB template.

As of this writing, you want version 2.1.0.pre of axlsx, so add this to your Gemfile:

# Export data to spreadsheets
gem 'axlsx', '2.1.0.pre'
gem 'axlsx_rails'

Then, create a view file just as you would for HTML. Here's a generic index.xlsx.axlsx view template for dumping a record's attributes:

# app/views/base_controller/index.xlsx.axlsx

# Hook into the xlsx package provided by the axlsx-rails gem
wb = xlsx_package.workbook

# Create a style we can use for record headers
styles = xlsx_package.workbook.styles
header_style = styles.add_style bg_color: "00",
                                fg_color: "FF",
                                bold: true,
                                alignment: { horizontal: :center }

# Get a list of associations. We want to export the association value rather than ID
# e.x. Get Post#user rather than Post#user_id
assocs = resource_class.reflect_on_all_associations(:belongs_to).map{|a| [a.foreign_key.to_sym, a.name]}.to_h

# Create a worksheet. Name it after the resource we're exporting
wb.add_worksheet(name: @resource.model_name.plural.titleize) do |sheet|
  # Get list of attributes to export
  attrs = policy(@resources.first).permitted_export_attributes

  # Add a header row using the header style we defined
  sheet.add_row attrs.map { |n| @resources.first.class.human_attribute_name(n) }, style: header_style

  # Add each row to our sheet
  @resources.each do |row|
    sheet.add_row attrs.map { |a| row.send(assocs[a] || a) }
  end
end

One thing of note is the policy(@resource_value.first).permitted_export_attributes line. This allows us to define a method in Pundit policies that returns a list of fields allowed for export. In application_policy.rb one might add something like:

# app/policies/application_policy.rb

def permitted_export_attributes
  # don't export friendly_id slug, or legacy data mapping fields.
  record.class.attribute_names.map(&:to_sym) - %i(slug legacy_id data_source)
end

One last thing I found useful was to set the Content-Disposition to attachment and set my own export file name. This triggers file download (rather than the user having to right click, save as) and allows us to set a sensible file name.

# app/controllers/people_controller.rb

def index
  respond_to do |format|
    format.html
    format.xlsx { set_attachment_name "sites #{Time.now.utc.strftime('%Y%M%d%H%M%S')}.xlsx" }
  end
end
# app/controllers/base_controller.rb

protected

def set_attachment_name(name)
  escaped = URI.encode(name)
  response.headers['Content-Disposition'] = "attachment; filename*=UTF-8''#{escaped}"
end

And here we go! sites 20172419062423.xlsx

Screen Shot 2017-02-19 at 13.25.05.png