From 7097470b28d05d2d229bb29438114a4862879c24 Mon Sep 17 00:00:00 2001 From: Philippe Creux Date: Wed, 25 May 2011 06:52:05 -0700 Subject: [PATCH] CSV is now customizable --- Gemfile | 1 + README.rdoc | 9 ++ features/comments/commenting.feature | 2 +- features/index/format_as_csv.feature | 20 ++++- features/step_definitions/format_steps.rb | 26 ++++-- lib/active_admin.rb | 2 +- lib/active_admin/csv_builder.rb | 45 ++++++++++ lib/active_admin/dsl.rb | 13 +++ lib/active_admin/resource.rb | 14 +++- lib/active_admin/resource_controller.rb | 9 -- lib/active_admin/table_to_csv.rb | 44 ---------- lib/active_admin/views/pages/index.rb | 17 ++-- .../active_admin/resource/index.csv.erb | 17 +++- .../active_admin_default/index.csv.arb | 8 -- spec/controllers/index_as_csv_spec.rb | 35 -------- spec/unit/csv_builder_spec.rb | 83 +++++++++++++++++++ spec/unit/resource_spec.rb | 16 ++++ spec/unit/table_to_csv_spec.rb | 49 ----------- 18 files changed, 239 insertions(+), 171 deletions(-) create mode 100644 lib/active_admin/csv_builder.rb delete mode 100644 lib/active_admin/table_to_csv.rb delete mode 100644 lib/active_admin/views/templates/active_admin_default/index.csv.arb delete mode 100644 spec/controllers/index_as_csv_spec.rb create mode 100644 spec/unit/csv_builder_spec.rb delete mode 100644 spec/unit/table_to_csv_spec.rb diff --git a/Gemfile b/Gemfile index 4fbcb1d8f89..1604567418f 100644 --- a/Gemfile +++ b/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', :platforms => :ruby_18 group :development, :test do gem 'sqlite3-ruby', :require => 'sqlite3' diff --git a/README.rdoc b/README.rdoc index ca5e74a41f5..ce13e84c18b 100644 --- a/README.rdoc +++ b/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 diff --git a/features/comments/commenting.feature b/features/comments/commenting.feature index 64cddb1a4d2..96e3a6fa145 100644 --- a/features/comments/commenting.feature +++ b/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" diff --git a/features/index/format_as_csv.feature b/features/index/format_as_csv.feature index 765382d5da7..010bef32944 100644 --- a/features/index/format_as_csv.feature +++ b/features/index/format_as_csv.feature @@ -9,10 +9,24 @@ Feature: Format as CSV 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 | + | Id | Title | Body | Published At | Created At | Updated At | | \d+ | Hello World | | | (.*) | (.*) | - Scenario: With index customization - 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 | diff --git a/features/step_definitions/format_steps.rb b/features/step_definitions/format_steps.rb index a4b57d1f0cf..2e4d52a6a9e 100644 --- a/features/step_definitions/format_steps.rb +++ b/features/step_definitions/format_steps.rb @@ -6,14 +6,24 @@ Then %{I should see "#{format_type}" within "#index_footer a"} end -require 'csv' - Then /^I should see the CSV:$/ do |table| - puts page.body - csv = CSV.parse(page.body) - csv.each_with_index do |row, row_index| - row.each_with_index do |cell, col_index| - cell.should match(/#{table.raw[row_index][col_index]}/) - end + 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 diff --git a/lib/active_admin.rb b/lib/active_admin.rb index 5ddb726af92..0e97d0fcc94 100644 --- a/lib/active_admin.rb +++ b/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' @@ -29,7 +30,6 @@ module ActiveAdmin autoload :Scope, 'active_admin/scope' autoload :Sidebar, 'active_admin/sidebar' autoload :TableBuilder, 'active_admin/table_builder' - autoload :TableToCSV, 'active_admin/table_to_csv' autoload :ViewFactory, 'active_admin/view_factory' autoload :ViewHelpers, 'active_admin/view_helpers' autoload :Views, 'active_admin/views' diff --git a/lib/active_admin/csv_builder.rb b/lib/active_admin/csv_builder.rb new file mode 100644 index 00000000000..da1801e6f9d --- /dev/null +++ b/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 diff --git a/lib/active_admin/dsl.rb b/lib/active_admin/dsl.rb index 331a2d0abdd..6684ab5e9f9 100644 --- a/lib/active_admin/dsl.rb +++ b/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. diff --git a/lib/active_admin/resource.rb b/lib/active_admin/resource.rb index 149e227a28c..206a5c5969c 100644 --- a/lib/active_admin/resource.rb +++ b/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 diff --git a/lib/active_admin/resource_controller.rb b/lib/active_admin/resource_controller.rb index bc70548e2ef..9c438858920 100644 --- a/lib/active_admin/resource_controller.rb +++ b/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 diff --git a/lib/active_admin/table_to_csv.rb b/lib/active_admin/table_to_csv.rb deleted file mode 100644 index 6682e52fa9d..00000000000 --- a/lib/active_admin/table_to_csv.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActiveAdmin - - # Convert an Arbre::HTML::Table to CSV - class TableToCSV - include ActionView::Helpers::SanitizeHelper - - def initialize(table) - @table = table - end - - def to_s - CSV.generate do |csv| - - thead = @table.find_by_tag("thead").first - csv << thead.find_by_tag("th").collect do |th| - strip_content th.content - end - - tbody = @table.find_by_tag("tbody").first - tbody.find_by_tag("tr").each do |tr| - row = tr.find_by_tag("td").map do |td| - strip_content td.content - end - csv << row - end - end - end - - def current_dom_context - self.to_s - end - - private - - def strip_content(html) - strip_tags(strip_indentation(html)) - end - - def strip_indentation(html) - html.split("\n").map{|string| string.strip }.join("\n") - end - end - -end diff --git a/lib/active_admin/views/pages/index.rb b/lib/active_admin/views/pages/index.rb index 99f468a04d1..6be85fc941c 100644 --- a/lib/active_admin/views/pages/index.rb +++ b/lib/active_admin/views/pages/index.rb @@ -26,6 +26,14 @@ def main_content end end + protected + + def build_scopes + if active_admin_config.scopes.any? + scopes_renderer active_admin_config.scopes + end + end + # Creates a default configuration for the resource class. This is a table # with each column displayed as well as all the default actions def default_index_config @@ -38,15 +46,6 @@ def default_index_config end end - protected - - def build_scopes - if active_admin_config.scopes.any? - scopes_renderer active_admin_config.scopes - end - end - - # Returns the actual class for renderering the main content on the index # page. To set this, use the :as option in the page_config block. def find_index_renderer_class(symbol_or_class) diff --git a/lib/active_admin/views/templates/active_admin/resource/index.csv.erb b/lib/active_admin/views/templates/active_admin/resource/index.csv.erb index 1bc67e6f432..e2978e48e18 100644 --- a/lib/active_admin/views/templates/active_admin/resource/index.csv.erb +++ b/lib/active_admin/views/templates/active_admin/resource/index.csv.erb @@ -1,2 +1,15 @@ -<%= @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 %> +<%- + # FasterCSV is CSV in ruby1.9 + csv_lib = defined?(FasterCSV) ? 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 %> diff --git a/lib/active_admin/views/templates/active_admin_default/index.csv.arb b/lib/active_admin/views/templates/active_admin_default/index.csv.arb deleted file mode 100644 index 823f6574c41..00000000000 --- a/lib/active_admin/views/templates/active_admin_default/index.csv.arb +++ /dev/null @@ -1,8 +0,0 @@ -page_config = active_admin_config.page_configs[:index] || ActiveAdmin::PageConfig.new(:as => table) do |display| - id_column - resource_class.content_columns.each do |col| - column col.name.to_sym - end -end -index_table = insert_tag(ActiveAdmin::Views::IndexAsTable, page_config, collection) -@__current_dom_element__ = ActiveAdmin::TableToCSV.new(index_table) diff --git a/spec/controllers/index_as_csv_spec.rb b/spec/controllers/index_as_csv_spec.rb deleted file mode 100644 index d2df835a2f3..00000000000 --- a/spec/controllers/index_as_csv_spec.rb +++ /dev/null @@ -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 diff --git a/spec/unit/csv_builder_spec.rb b/spec/unit/csv_builder_spec.rb new file mode 100644 index 00000000000..aca35ef7555 --- /dev/null +++ b/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 diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index a0ef7854775..97ad26adc69 100644 --- a/spec/unit/resource_spec.rb +++ b/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 diff --git a/spec/unit/table_to_csv_spec.rb b/spec/unit/table_to_csv_spec.rb deleted file mode 100644 index db109605de9..00000000000 --- a/spec/unit/table_to_csv_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::TableToCSV do - include Arbre::HTML - - let(:assigns){ {} } - let(:helpers){ action_view } - - let(:post_1){ Post.new(:title => "Hello world")} - let(:post_2){ Post.new(:title => "Hello world 2")} - - let(:csv){ ActiveAdmin::TableToCSV.new(table).to_s } - - - describe "a basic table" do - let :table do - table_for [post_1, post_2] do - column :title - column :body - end - end - - - it "should render the table headers" do - csv.split("\n").first.should == "Title,Body" - end - - it "should render the first row" do - csv.split("\n")[1].should == %{Hello world,""} - end - end - - describe "a table with html" do - let :table do - table_for [post_1, post_2] do - column("Title".html_safe){|p| a p.title, :href => "/woot" } - column(:body){|p| para p.body } - end - end - - it "should render content without html tags" do - csv.split("\n")[1].should == %{Hello world,""} - end - it "should render headers without html tags" do - csv.split("\n")[0].should == %{Title,Body} - end - end - -end