Import a CSV with associations?
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 '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
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:
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
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
- 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!
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.
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])
Thanks Chris! Between you and some friendly SO folks I got it working. You sir are the man.
Cheers.