Cory O'Daniel – These are just words Software development, thoughts, and randomness

27Jan/101

An Excel CSV exporter for ActiveRecord

This is a mix of two blog posts on exporting ActiveRecord data to CSV. This explicitly was designed to export stuff so excel wouldn't freak out.

This strips new lines from any string field, set the BOM and converts the encoding to utf16.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
require "fastercsv"
require "iconv"
 
# Excel info from: http://blog.plataformatec.com.br/2009/09/exporting-data-to-csv-and-excel-in-your-rails-app/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed:+PlataformaBlog+(Plataforma+Blog)
# Original post: http://www.brynary.com/2007/4/28/export-activerecords-to-csv
 
# Usage:
#   Given User is an ActiveRecord model
#   
#   Options for Model.to_csv, Array.to_csv, and ActionController::Base#send_csv
# 
#   :only     - Array, trumps :exclude. Only these attributes will be included instead of all attributes (things listed in :methods will still be sent)
#     :only => [ :id, :name, :created_at ]
#   :methods  - Array, additional methods to evaluate and add to the CSV response. Note: you can send nested method calls
#     :methods => [ :to_s, :complex_method, "my.method.on.a.related.object"]
#   :exclude  - Array, attributes to exclude from CSV result
# 
#  From ActionController
#     send_csv User, :only => [:first_name, :email, :created_at] #This will do all records
#     send_csv User.all(:conditions => ["created_at > ?", some_date]), :only => [:first_name, :email, :created_at]
#
#  From ActiveRecord Model
#   User.to_csv :only => [ :username, :email, :created_at ], :methods => [ :age, "account.id"] # All users additionally get their related Account#id number
#   User.all(:limit => 10).to_csv :exclude => [:password, :birthdate]
#
class ActiveRecord::Base
 
  # Shortcut for CSV of whole table
  def self.to_csv(*args)
    find(:all).to_csv(*args)
  end
 
  # Get the column headers
  def self.csv_columns(options={})
    tmp_columns = if options[:only]
      options[:only] #only trumps exclude
    else
      self.content_columns.map{|curr_col| curr_col.name } - options[:exclude].map{|curr_col| curr_col.to_s }
    end
 
    tmp_columns + options[:methods].map{|curr_col| curr_col.to_s }
  end
 
  # Record to a row level csv array
  def to_csv(options={})
    self.class.csv_columns(options).map { |curr_col|
      curr_col = curr_col.to_s  
 
      #Its a chain of method calls, on intermediary nil, just return nil
      if !curr_col.index(".")
        col_val = self.send(curr_col) 
      else
        col_val = self
        curr_col.split(".").each {|curr_method| col_val = col_val.send(curr_method) unless col_val.nil?}
      end
 
      col_val.gsub!("\n", " ") if col_val.is_a?(String) # Strip newlines, Appease Excel Gods      
      col_val
    }
  end
end
 
 
class Array
 
  # Convert an array of objects into a CSV String.
  def to_csv(options = {})
    column_options    = {
      :only    => options.delete(:only),
      :exclude => options.delete(:exclude) || [],
      :methods => options.delete(:methods) || []
    }
 
    options = {
      :col_sep => "\t" # Appease Excel Gods
    }.merge(options)
 
    if all? { |e| e.respond_to?(:to_csv) }
      header_row = first.class.csv_columns(column_options).to_csv
 
      content_rows = map { |e| 
        # Get all the values of non-excluded rows
        e.to_csv(column_options) 
      }.map{|r| 
        # Call to_csv on the array of row-level values, this will join them into a CSV row
        r.to_csv(column_options) 
      }
 
      ([header_row] + content_rows).join
    else
      FasterCSV.generate_line(self, options)
    end
  end
 
end
 
 
# module for extending ActionController
module ExcelCSVExporter
  BOM = "\377\376" #Byte Order Mark, Appease Excel Gods
 
  def send_csv(kollection, options={})
    filename = options.delete(:filename) || I18n.l(Time.now, :format => :short) + ".csv"
 
    content = kollection.to_csv(options)
 
    # Appease Excel Gods
    content = BOM + Iconv.conv("utf-16le", "utf-8", content)
    send_data content, :filename => filename    
  end
end
 
ActionController::Base.send :include, ExcelCSVExporter

Wanna use it?

1
2
3
4
5
6
7
class MyCoolController < ActionController::Base
  def index
    respond_to {|want|
      want.csv{ send_csv MyCoolModel.all, :exclude => [:secret_column], :methods => [:complex_method, "related.table.method"]}
    }
  end
end

Post to Twitter Post to Digg Post to Facebook Post to Reddit Post to StumbleUpon

Comments (1) Trackbacks (0)
  1. I’ll add :methods and :o nly support in the next day or so . . . needs a little cleaning up


Leave a comment


No trackbacks yet.