Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Make CSV format customizable #138

Merged
merged 4 commits into from

7 participants

@pcreux
Collaborator

Want to customize the CSV output to something really usefull?

Syntax is:

ActiveAdmin::Resource.register Post do
  csv do
    column :name
    column("Author") { |post| post.author.full_name }
  end
end

Boom!

@pcreux
Collaborator

Works both on 1.8 and 1.9 now. FasterCSV is always installed and required. We switch between FasterCSV and CSV using the RUBY_VERSION when rendering the CSV.

@bbonamin

This is excellent!

@gregbell gregbell merged commit 44dbeeb into from
@aarondmills

Is there a way to make this conditional so that it works even if its nil? It breaks if a field in the model is blank

@pcreux
Collaborator

Hello Aaron,

I can't reproduce this issue. The following test works fine for me:

  Scenario: With CSV format customization
    Given a configuration of:
    """
      ActiveAdmin.register Post do
        csv do
          column :title
          column :body   # nil
          column("Last update") { |post| post.updated_at }
          column("Copyright")   { "Greg Bell" }
        end
      end
    """
    And a post with the title "Hello, World" exists
    When I am on the index page for posts
    And I follow "CSV"
    And I should download a CSV file for "posts" containing:
    | Title        | Body | Last update | Copyright |
    | Hello, World |      | (.*)        | Greg Bell |

Which versions of Ruby, Rails and ActiveAdmin are you using? Could you paste a copy of the csv customization you are using?

@cmassao

how to get it in UTF-8 ??

,Maçao Saito,Mumificação Pulpar

@willywg

I have the same problem with UTF-8

@redbar0n

Yes, how do we get in UTF-8 ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 31, 2011
  1. @gregbell @pcreux

    Initial commit for CSV configuration

    gregbell authored pcreux committed
  2. @gregbell @pcreux

    Got initial cuke passing

    gregbell authored pcreux committed
  3. @pcreux

    CSV is now customizable

    pcreux authored
  4. @pcreux
This page is out of date. Refresh to see the latest.
View
1  Gemfile
@@ -30,6 +30,7 @@ gem 'formtastic', '>= 1.1.0'
gem 'will_paginate', '>= 3.0.pre2'
gem 'inherited_resources'
gem 'sass', '>= 3.1.0'
+gem 'fastercsv'
group :development, :test do
gem 'sqlite3-ruby', :require => 'sqlite3'
View
9 README.rdoc
@@ -291,6 +291,15 @@ the collection as a proc to be called at render time.
# Will call available
filter :author, :as => :check_boxes, :collection => proc { Author.all }
+== Customizing the CSV format
+
+Customizing the CSV format is as simple as customizing the index page.
+
+ csv do
+ column :name
+ column("Author") { |post| post.author.full_name }
+ end
+
== Customizing the Form
Active Admin gives complete control over the output of the form by creating a thin DSL on top of
View
12 activeadmin.gemspec
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
"features/edit_page.feature",
"features/first_boot.feature",
"features/global_navigation.feature",
+ "features/index/format_as_csv.feature",
"features/index/formats.feature",
"features/index/index_as_block.feature",
"features/index/index_as_blog.feature",
@@ -96,6 +97,7 @@ Gem::Specification.new do |s|
"lib/active_admin/comments/views/active_admin_comments.rb",
"lib/active_admin/component.rb",
"lib/active_admin/controller_action.rb",
+ "lib/active_admin/csv_builder.rb",
"lib/active_admin/dashboards.rb",
"lib/active_admin/dashboards/dashboard_controller.rb",
"lib/active_admin/dashboards/section.rb",
@@ -210,7 +212,6 @@ Gem::Specification.new do |s|
"lib/generators/active_admin/install/templates/migrations/2_move_admin_notes_to_comments.rb",
"lib/generators/active_admin/resource/resource_generator.rb",
"lib/generators/active_admin/resource/templates/admin.rb",
- "spec/controllers/index_as_csv_spec.rb",
"spec/integration/belongs_to_spec.rb",
"spec/spec_helper.rb",
"spec/support/integration_example_group.rb",
@@ -238,6 +239,7 @@ Gem::Specification.new do |s|
"spec/unit/components/sidebar_section_spec.rb",
"spec/unit/components/table_for_spec.rb",
"spec/unit/controller_filters_spec.rb",
+ "spec/unit/csv_builder_spec.rb",
"spec/unit/dashboard_controller_spec.rb",
"spec/unit/dashboard_section_spec.rb",
"spec/unit/dashboards_spec.rb",
@@ -261,10 +263,9 @@ Gem::Specification.new do |s|
]
s.homepage = %q{http://github.com/gregbell/active_admin}
s.require_paths = ["lib"]
- s.rubygems_version = %q{1.7.2}
+ s.rubygems_version = %q{1.3.7}
s.summary = %q{The administration framework for Ruby on Rails.}
s.test_files = [
- "spec/controllers/index_as_csv_spec.rb",
"spec/integration/belongs_to_spec.rb",
"spec/spec_helper.rb",
"spec/support/integration_example_group.rb",
@@ -292,6 +293,7 @@ Gem::Specification.new do |s|
"spec/unit/components/sidebar_section_spec.rb",
"spec/unit/components/table_for_spec.rb",
"spec/unit/controller_filters_spec.rb",
+ "spec/unit/csv_builder_spec.rb",
"spec/unit/dashboard_controller_spec.rb",
"spec/unit/dashboard_section_spec.rb",
"spec/unit/dashboards_spec.rb",
@@ -315,6 +317,7 @@ Gem::Specification.new do |s|
]
if s.respond_to? :specification_version then
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
@@ -325,6 +328,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency(%q<will_paginate>, [">= 3.0.pre2"])
s.add_runtime_dependency(%q<inherited_resources>, [">= 0"])
s.add_runtime_dependency(%q<sass>, [">= 3.1.0"])
+ s.add_runtime_dependency(%q<fastercsv>, [">= 0"])
s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
s.add_development_dependency(%q<jeweler>, ["= 1.5.2"])
s.add_development_dependency(%q<rake>, ["= 0.8.7"])
@@ -337,6 +341,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<will_paginate>, [">= 3.0.pre2"])
s.add_dependency(%q<inherited_resources>, [">= 0"])
s.add_dependency(%q<sass>, [">= 3.1.0"])
+ s.add_dependency(%q<fastercsv>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
s.add_dependency(%q<jeweler>, ["= 1.5.2"])
s.add_dependency(%q<rake>, ["= 0.8.7"])
@@ -350,6 +355,7 @@ Gem::Specification.new do |s|
s.add_dependency(%q<will_paginate>, [">= 3.0.pre2"])
s.add_dependency(%q<inherited_resources>, [">= 0"])
s.add_dependency(%q<sass>, [">= 3.1.0"])
+ s.add_dependency(%q<fastercsv>, [">= 0"])
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
s.add_dependency(%q<jeweler>, ["= 1.5.2"])
s.add_dependency(%q<rake>, ["= 0.8.7"])
View
2  features/comments/commenting.feature
@@ -81,5 +81,5 @@ Feature: Commenting
Scenario: Viewing all commments for a namespace
When I add a comment "Hello from Comment"
When I am on the index page for comments
- Then I should see a table header for "Body"
+ Then I should see a table header with "Body"
And I should see "Hello from Comment"
View
32 features/index/format_as_csv.feature
@@ -0,0 +1,32 @@
+Feature: Format as CSV
+
+ Scenario: Default with no index customization
+ Given an index configuration of:
+ """
+ ActiveAdmin.register Post
+ """
+ And a post with the title "Hello World" exists
+ When I am on the index page for posts
+ And I follow "CSV"
+ Then I should see the CSV:
+ | Id | Title | Body | Published At | Created At | Updated At |
+ | \d+ | Hello World | | | (.*) | (.*) |
+
+ Scenario: With CSV format customization
+ Given an index configuration of:
+ """
+ ActiveAdmin.register Post do
+ csv do
+ column :title
+ column("Last update") { |post| post.updated_at }
+ column("Copyright") { "Greg Bell" }
+ end
+ end
+ """
+ And a post with the title "Hello World" exists
+ When I am on the index page for posts
+ And I follow "CSV"
+ Then I should see the CSV:
+ | Title | Last update | Copyright |
+ | Hello World | (.*) | Greg Bell |
+
View
22 features/step_definitions/format_steps.rb
@@ -5,3 +5,25 @@
Then /^I should see a link to download "([^"]*)"$/ do |format_type|
Then %{I should see "#{format_type}" within "#index_footer a"}
end
+
+Then /^I should see the CSV:$/ do |table|
+ begin
+ csv = CSV.parse(page.body)
+ csv.each_with_index do |row, row_index|
+ row.each_with_index do |cell, col_index|
+ expected_cell = table.raw.try(:[], row_index).try(:[], col_index)
+ if expected_cell.blank?
+ cell.should be_nil
+ else
+ cell.should match(/#{expected_cell}/)
+ end
+ end
+ end
+ rescue
+ puts "Expecting:"
+ p table.raw
+ puts "to match:"
+ p csv
+ raise $!
+ end
+end
View
1  lib/active_admin.rb
@@ -13,6 +13,7 @@ module ActiveAdmin
autoload :Callbacks, 'active_admin/callbacks'
autoload :Component, 'active_admin/component'
autoload :ControllerAction, 'active_admin/controller_action'
+ autoload :CSVBuilder, 'active_admin/csv_builder'
autoload :Dashboards, 'active_admin/dashboards'
autoload :Devise, 'active_admin/devise'
autoload :DSL, 'active_admin/dsl'
View
45 lib/active_admin/csv_builder.rb
@@ -0,0 +1,45 @@
+module ActiveAdmin
+ # CSVBuilder stores CSV configuration
+ #
+ # Usage example:
+ #
+ # csv_builder = CSVBuilder.new
+ # csv_builder.column :id
+ # csv_builder.column("Name") { |resource| resource.full_name }
+ #
+ class CSVBuilder
+
+ # Return a default CSVBuilder for a resource
+ # The CSVBuilder's columns would be Id followed by this
+ # resource's content columns
+ def self.default_for_resource(resource)
+ new.tap do |csv_builder|
+ csv_builder.column(:id)
+ resource.content_columns.each do |content_column|
+ csv_builder.column(content_column.name.to_sym)
+ end
+ end
+ end
+
+ attr_reader :columns
+
+ def initialize(&block)
+ @columns = []
+ instance_eval &block if block_given?
+ end
+
+ # Add a column
+ def column(name, &block)
+ @columns << Column.new(name, block)
+ end
+
+ class Column
+ attr_reader :name, :data
+
+ def initialize(name, block = nil)
+ @name = name.is_a?(Symbol) ? name.to_s.titleize : name
+ @data = block || name.to_sym
+ end
+ end
+ end
+end
View
13 lib/active_admin/dsl.rb
@@ -115,6 +115,19 @@ def form(options = {}, &block)
controller.form_config = options
end
+ # Configure the CSV format
+ #
+ # For example:
+ #
+ # csv do
+ # column :name
+ # column("Author") { |post| post.author.full_name }
+ # end
+ #
+ def csv(&block)
+ config.csv_builder = CSVBuilder.new(&block)
+ end
+
# Member Actions give you the functionality of defining both the
# action and the route directly from your ActiveAdmin registration
# block.
View
14 lib/active_admin/resource.rb
@@ -46,6 +46,8 @@ class Resource
# Set to false to turn off admin notes
attr_accessor :admin_notes
+ # Set the configuration for the CSV
+ attr_writer :csv_builder
def initialize(namespace, resource, options = {})
@namespace = namespace
@@ -189,6 +191,11 @@ def belongs_to?
!belongs_to_config.nil?
end
+ # The csv builder for this resource
+ def csv_builder
+ @csv_builder || default_csv_builder
+ end
+
private
def default_options
@@ -198,5 +205,8 @@ def default_options
}
end
- end
-end
+ def default_csv_builder
+ @default_csv_builder ||= CSVBuilder.default_for_resource(resource)
+ end
+ end # class Resource
+end # module ActiveAdmin
View
9 lib/active_admin/resource_controller.rb
@@ -21,7 +21,6 @@ class ResourceController < ::InheritedResources::Base
before_filter :only_render_implemented_actions
before_filter :authenticate_active_admin_user
- before_filter :prepare_csv_columns, :only => :index
include ActiveAdmin::ActionItems
include ActionBuilder
@@ -116,13 +115,5 @@ def renderer_for(action)
ActiveAdmin.view_factory["#{action}_page"]
end
helper_method :renderer_for
-
- # Before filter to prepare the columns for CSV. Note this will
- # be deprecated very soon.
- def prepare_csv_columns
- if request.format.csv?
- @csv_columns = resource_class.columns.collect{ |column| column.name.to_sym }
- end
- end
end
end
View
16 lib/active_admin/views/templates/active_admin/resource/index.csv.erb
@@ -1,2 +1,14 @@
-<%= @csv_columns.collect{|c| c.to_s.titleize }.join(",").html_safe %>
-<%= collection.collect{|resource| @csv_columns.collect{|col| "\"#{resource.send(col).to_s.gsub('"', '""')}\""}.join(",") }.join("\n").html_safe %>
+<%-
+ csv_lib = RUBY_VERSION =~ /^1.8/ ? FasterCSV : CSV
+
+ csv_output = csv_lib.generate do |csv|
+ columns = active_admin_config.csv_builder.columns
+ csv << columns.map(&:name)
+ collection.each do |resource|
+ csv << columns.map do |column|
+ call_method_or_proc_on resource, column.data
+ end
+ end
+ end
+%>
+<%= csv_output %>
View
35 spec/controllers/index_as_csv_spec.rb
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe_with_render Admin::PostsController do
- describe "get index with format csv" do
-
- before do
- Post.create :title => "Hello World"
- Post.create :title => "Goodbye World"
- end
-
- it "should return csv" do
- get :index, 'format' => 'csv'
- response.content_type.should == 'text/csv'
- end
-
- it "should return a header and a line for each item" do
- get :index, 'format' => 'csv'
- response.body.split("\n").size.should == 3
- end
-
- Post.columns.each do |column|
- it "should include a header for #{column.name}" do
- get :index, 'format' => 'csv'
- response.body.split("\n").first.should include(column.name.titleize)
- end
- end
-
- it "should set a much higher per page pagination" do
- 100.times{ Post.create :title => "woot" }
- get :index, 'format' => 'csv'
- response.body.split("\n").size.should == 103
- end
-
- end
-end
View
83 spec/unit/csv_builder_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe ActiveAdmin::CSVBuilder do
+
+ describe '.default_for_resource using Post' do
+ let(:csv_builder) { ActiveAdmin::CSVBuilder.default_for_resource(Post) }
+
+ it "should return a default csv_builder for Post" do
+ csv_builder.should be_a(ActiveAdmin::CSVBuilder)
+ end
+
+ specify "the first column should be Id" do
+ csv_builder.columns.first.name.should == 'Id'
+ csv_builder.columns.first.data.should == :id
+ end
+
+ specify "the following columns should be content_column" do
+ csv_builder.columns[1..-1].each_with_index do |column, index|
+ column.name.should == Post.content_columns[index].name.titleize
+ column.data.should == Post.content_columns[index].name.to_sym
+ end
+ end
+ end
+
+ context 'when empty' do
+ let(:builder){ ActiveAdmin::CSVBuilder.new }
+
+ it "should have no columns" do
+ builder.columns.should == []
+ end
+ end
+
+ context "with a symbol column (:title)" do
+ let(:builder) do
+ ActiveAdmin::CSVBuilder.new do
+ column :title
+ end
+ end
+
+ it "should have one colum" do
+ builder.columns.size.should == 1
+ end
+
+ describe "the column" do
+ let(:column){ builder.columns.first }
+
+ it "should have a name of 'Title'" do
+ column.name.should == "Title"
+ end
+
+ it "should have the data :title" do
+ column.data.should == :title
+ end
+ end
+ end
+
+ context "with a block and title" do
+ let(:builder) do
+ ActiveAdmin::CSVBuilder.new do
+ column "My title" do
+ # nothing
+ end
+ end
+ end
+
+ it "should have one colum" do
+ builder.columns.size.should == 1
+ end
+
+ describe "the column" do
+ let(:column){ builder.columns.first }
+
+ it "should have a name of 'My title'" do
+ column.name.should == "My title"
+ end
+
+ it "should have the data :title" do
+ column.data.should be_an_instance_of(Proc)
+ end
+ end
+ end
+
+end
View
16 spec/unit/resource_spec.rb
@@ -248,6 +248,22 @@ module ::Mock; class Resource; end; end
config.get_scope_by_id(:published).name.should == "Published"
end
end
+
+ describe "#csv_builder" do
+ context "when no csv builder set" do
+ it "should return a default column builder with id and content columns" do
+ config.csv_builder.columns.size.should == Category.content_columns.size + 1
+ end
+ end
+
+ context "when csv builder set" do
+ it "shuld return the csv_builder we set" do
+ csv_builder = CSVBuilder.new
+ config.csv_builder = csv_builder
+ config.csv_builder.should == csv_builder
+ end
+ end
+ end
describe "admin notes" do
context "when not set" do
Something went wrong with that request. Please try again.