Ask A Question

Notifications

You’re not receiving notifications from this thread.

Import a CSV with associations?

Robert Guss asked in General

I am building a rails application that associates serial #'s with software titles. For instance, I have a software title, and need to be able to upload batches of serial #'s(codes) and associate it with that specific software title. It needs to be simple enough for a user(authenticated) to click an upload link, select a software title from a dropdown and hit import. Here is what I have so far... It does not necessarily have to be a csv it could be a text file too. I just need help figuring out the best way to accomplish this.

Code Upload UI

Code 'form' for UI

<%= simple_form_for(@code) do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <%= f.association :software %>
    <%= f.input :label %>
    <%= f.input :code %>
  </div>

  <div class="form-actions">
    <%= f.file_field :code %>
    <br>
    <%= f.button :submit, "Upload Codes", class: 'btn btn-warning' %>
  </div>
  <br>
<% end %>

Code.rb Model

class Code < ActiveRecord::Base
  belongs_to :software
  belongs_to :user
  accepts_nested_attributes_for :software

  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|
      Code.create! row.to_hash
    end
  end
end

Software.rb Model

class Software < ActiveRecord::Base
  has_many :software_assigns
  has_many :products, through: :software_assigns
  has_many :software_downloads
  has_many :codes
end

Codes Controller

class CodesController < ApplicationController
  before_action :authenticate_user!
  before_action :verify_admin
  before_action :set_code, only: [:show, :edit, :update, :destroy]

  # GET /codes
  # GET /codes.json
  def index
    @codes = Code.all
  end

  # GET /codes/1
  # GET /codes/1.json
  def show
  end

  # GET /codes/new
  def new
    @code = Code.new
  end

  # GET /codes/1/edit
  def edit
  end

  # POST /codes
  # POST /codes.json
  def create
    @code = Code.new(code_params)

    respond_to do |format|
      if @code.save
        format.html { redirect_to @code, notice: 'Codes were successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  # PATCH/PUT /codes/1
  # PATCH/PUT /codes/1.json
  def update
    respond_to do |format|
      if @code.update(code_params)
        format.html { redirect_to @code, notice: 'Codes were successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end

  # DELETE /codes/1
  # DELETE /codes/1.json
  def destroy
    @code.destroy
    respond_to do |format|
      format.html { redirect_to codes_url, notice: 'Codes were successfully destroyed.' }
    end
  end

  def import
    Code.import(params[:file])
    redirect_to codes_path, notice: 'Codes were successfully uploaded!'
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_code
      @code = Code.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def code_params
      params.require(:code).permit(:software_id, :label, :code)
    end
end
Reply

Sup Robert!

So the main thing here is just to make sure that your import code sets up the association between the software you select in the UI and the codes that are imported.

You will want to adjust your import code accordingly in order to do that, so here's what I'd suggest:

  1. It looks like your view should have a select for the software_id so your controller can look up the appropriate software. Looks like you've got that in there with the f.association

  2. When it hits your controller, you'll want to first lookup the software these codes should be associated with. Then you'll pass that software as an additional argument into the import code.

  def import
    @software = Software.find(params[:software_id])
    Code.import(@software, params[:file])
    redirect_to codes_path, notice: 'Codes were successfully uploaded!'
  end
  1. Your import method just needs to accept a new attribute and then reference the association when creating codes now:
  def self.import(software, file)
    CSV.foreach(file.path, headers: true) do |row|
      software.codes.create! row.to_hash
    end
  end

That should do the trick!

Reply

Hi Chris,

Let me first say thank you so much for getting back to me at all, let alone so fast! I am a big fan and love your videos and you have helped me a ton. I also just subscribed ;)

So I implemented your code and we are so close. The association works, however the 'code' field is an object in memory and looks like #<ActionDispatch::Http::UploadedFile:0x007fca7d9c8098> I also have this open on Stack Overflow and have received some good feedback as well. The codes are located here code_params[:code]. So when I upload my csv with several codes, I would like it to bring it all of them into their own rows each with the software ID assigned.

Also my csv file is a single column with a header of 'code' without quotes. Followed by 10 rows with unique serials like 111-222-333, etc. Just for testing.

Thanks again for all your help and keep up the great work! Cheers.

Reply

Thanks so much for the support man! I really appreciate it. :D

Ah yes, that's right. You'll want to use that instead of params[:file] in order to pass in the UploadedFile object. That's really just like a temporary file, so as long as you pass it in, you should be fine. I think if you change this in your controller, then this should work:

    Code.import(@software, code_params[:code])
Reply

Thanks Chris! Between you and some friendly SO folks I got it working. You sir are the man.

Cheers.

Reply

Robert - what was the solution? If you could paste it, it would def. help others here...

Reply
Hi Chris and Robert, this is awesome. This turned out to be the missing piece in my nested attribute journey
Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 87,563+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.