Home > Back-end >  Rails' export csv function "undefined method `export' for nil:NilClass"
Rails' export csv function "undefined method `export' for nil:NilClass"

Time:03-02

I'm making an export to csv file functionality in a Ruby on Rails repo and I'm almost done. However, when I press the "Export all" button, I get the undefined method `export' for nil:NilClass error. The log shows that format.csv { send_data @foos.export, filename: "foos-#{Date.today}.csv" } went wrong. What am I missing please?

This is model

class Foo < ApplicationRecord
  has_many :bars 

  def export
    [id, name, foos.map(&:name).join(' ')]
  end
end

This is part of controller

  def index
    @foos = Foo.all
  end

  def export
    all = Foo.all
    attributes = %w{name}
    CSV.generate(headers: true) do |csv|
      csv << attributes

      all.each do |foo|
        csv << attributes.map{ |attr| foo.send(attr) }
      end

      respond_to do |format|
          format.csv { send_data @foos.export, filename: "foos-#{Date.today}.csv" }
      end
    end
  end

  def name
    "#{foo_id} #{name}"
  end

This is View

<a href="/export.csv"><button >export all</button></a>

This is Routes

Rails.application.routes.draw do
  resources :foos
  
  get :export, controller: :foos

  root "foos#index"
end

This is Rake (lib/tasks/export.rb)

namespace :export do
    task foo: :environment do
      file_name = 'exported_foo.csv'
      csv_data = Foo.to_csv
   
      File.write(file_name, csv_data)
    end
  end

CodePudding user response:

Start by creating a service object that takes a collection of records and returns CSV so that you can test the CSV generation in isolation:

# app/services/foo_export_service.rb
# Just a Plain Old Ruby Object that converts a collection of foos into CSV
class FooExportService
  # The initializer gives us a good place to setup our service
  # @param [Enumerable] foo - an array or collection of records
  def initialize(foos) 
    @headers = %w{name} # the attributes you want to use
    @foos = foos
  end

  # performs the actual work
  # @return [String]
  def perform
     CSV.generate do |csv|
       @foos.each do |foo|
         csv << foo.serializable_hash.slice(@headers).values
       end
     end
  end

  # A convenient factory method which makes stubbing the 
  # service easier
  # @param [Enumerable] foos - an array or collection of records
  # @return [String]
  def self.perform(foos)
    new(foos).perform
  end
end
# example usage
FooExportService.perform(Foo.all)

Not everything in a Rails application needs to be jammed into a model, view or controller. They already have enough responsiblities. This also lets you resuse the code for example in your rake task if you actually need it.

This simply iterates over the collection and uses Rails built in serialization features to turn the model instances into hashes that can be serialized as CSV. It also uses the fact that Hash#slice also reorders the hash keys.

In your controller you then just use the service object:

class FoosController
  def export
    @foos = Foo.all
    respond_to do |format|
      format.csv do 
        send_data FooExportService.perform(@foos), 
          filename: "foos-#{Date.today}.csv" 
      end
    end
  end
end 

You don't even really need a separate export action in the first place. Just use MimeResponds to add CSV as an availble response format to the index:

class FoosController
  def index
    # GET /foos 
    # GET /foos.csv 
    @foos = Foo.all
    respond_to do |format|
      format.html
      format.csv do 
         send_data FooExportService.perform(@foos), 
            filename: "foos-#{Date.today}.csv" 
      end
    end
  end
end 
<%= link_to("Export as CSV", foos_path(format: :csv)) %>
  • Related