Rails CSV import - association params - ruby-on-rails

I am trying to figure out how to import a CSV file to create new records and make the appropriate associations.
I have 3 models:
1) Farmer (has_many :farms)
2) Farm (has_many :crops, belongs_to :farmer)
3) Crop (belongs_to :farm)
The goal is to have the user create each farm manually, and then import the crops though a CSV file. I cannot figure out how to pass the farm_id to the crops getting uploaded, creating the association.
The import action on the Crops controller:
def import
Crop.import(params[:file], params[:farm_id])
redirect_to root_url, notice: "Crops imported."
end
The import method in the Crop model:
def self.import(file, farm)
CSV.foreach(file.path, headers: true, encoding:'iso-8859-1:utf-8') do |row|
row["farm_id"] = farm
Crop.create! row.to_hash
end
end
The form for the upload on the farms#show page:
##farm = Farm.find(params[:id])
<%= form_tag import_crops_path, multipart: true do %>
<%= file_field_tag :file %>
<%= submit_tag "import CSV" %>
How do you send over the #farm.id on the farms#show page to import#crops along with the file? Is it best to do this through the form - or some other way?

You could do this a couple ways, it depends on how your routes are set up.
If you have something like
resources :farms do
resources :crops
end
import_crops_path(#farm, #crop) will generate a url along the lines of /farms/:farm_id/crops/new
with both options you can get the farm id with params[:farm_id]
Another option is to use a hidden field.
f.hidden_field(:farm, :id)
this will create a html field
<input type="hidden" id="farm_id" name"farm[id]" value="#{#farm.id}" />
With both options you can get the farm id with params[:farm_id]
References
https://apidock.com/rails/ActionView/Helpers/FormHelper/hidden_field
http://guides.rubyonrails.org/routing.html

Related

Is there a way to have multiple file uploads turn into multiple models

So I have a model called Photo and obviously I don't want to upload one photo at a time, so I replaced the new photo form with multi-file uploading. I am not quite sure how I can make these file uploads turn into unique photo models.
<%= bootstrap_form_with(:model => photo, :local => true) do |form| %>
<%= form.file_field :image, :multiple => true, :direct_upload => true %>
<%= form.submit %>
<% end %>
Edit: I am using Active Storage on Rails 6.0.0 rc2
You can't make multiple photo models. You probally mean multiple records. A model is a blueprint of the table in your database.
Check your terminal logs when you submit the form and you will probally see that in the params you will have something like: photo => [ files here ]
So in your controller create you have to loop through the array and create a photo record for each photo, something like this:
def create
params[:photo].each do |photo|
Photo.create(file: photo.file)
end
For those that come across this problem, here was my solution:
def create
photo_params[:images].each do |image|
#photo = current_user.photos.build
#photo.image = image
#photo.save
end
end

Upload Image with Active Record and Save Item with Reference to Session Model

2 questions really:
1) I am trying to save a note that belongs to the Client model (which also is used as login to create the session - therefore session[:id] = #client.id)
2)The note is an image, uploaded using ActiveRecord model.
Post request seems to be working fine but the note doesn't save. I am trying to only save the image first and client.id reference at first. The rest of the columns in the model will be filled in later using edit.
db schema contains the 2 ActiveRecord tables.
There seems to be no errors coming up but no new note is created (#note.save = false), which could be because currently nothing is being saved to a new note, just a new blob... in cmd you can see blob creation insert works fine, and then the next transaction is rolledback.
How would I add the logic to save #client.id in the note?
Below my code snippets...
the form for from the view:
<div class="image-upload">
<h3>Upload image of sick note</h3>
<div class="signup-form">
<%= form_for(#note) do |f| %>
<%= f.file_field :image %>
<%= f.submit "Load Sick Note", class: "btn-submit" %>
<% end %>
</div>
</div>
The create in the controller:
def create
#note = Note.new(note_params)
if #note.save
redirect_to '/'
else
redirect_to '/dashboard'
end
end
private
def note_params
params.require(:note).permit(:image)
end
Note model:
class Note < ApplicationRecord
belongs_to :client
has_one_attached :image
end
Instead of
#note = Note.new(note_params)
try
#note = Client.new.notes.build(note_params)
the .notes method (i.e. "Client.new.notes" ) comes from assuming you have a has_many :notes declaration in your Client model.
If you are only using a has_one :note declaration in your Client model then the .build() method will not work.

Rails 4 - Delayed_Job for CSV import

I'm building a marketplace app in Rails 4 where sellers can list items to sell. I have a csv import feature so sellers can bulk load products. The import code worked fine on small files but I ran into timeout issues with larger files. So I want to use delayed_job to process these files in the background.
I set up delayed_job up to the point where the job is queued (i see the job in the delayed_job table). But when I run the job, I get an error saying that the file to be imported is not found. It is looking for the file in a temp folder which doesn't exist when the job is run.
How do I save (or without saving) the file in a location where delayed_job can access it? And how to I tell delayed_job where the file is located?
my listings controller:
def import
Listing.import(params[:file], params[:user_id])
redirect_to seller_url, notice: "Products are being imported."
end
my listing model:
class Listing < ActiveRecord::Base
require 'csv'
require 'open-uri'
class << self
def importcsv(file_path)
CSV.foreach(file_path, headers: true, skip_blanks: true) do |row|
#some model processing
end
end
handle_asynchronously :importcsv
end
# My importer as a class method
def self.import(file, user_id)
Listing.importcsv file.path
end
end
Here is form view:
<%= form_tag import_listings_path, multipart: true do %>
<%= file_field_tag :file %>
<%= hidden_field_tag :user_id, current_user.id %>
<%= submit_tag "Import CSV" %>
<% end %>
Presumably the file is a form upload. I think those files only persist while the web request is running. My recommendation is to use FileUtils.copy to copy the file into some location that will exist when your job runs.
So, probably you don't want to handle_asynchronously importcsv, but instead copy the files then call a private method on your model (which will be handled asynchronously) with the new file paths.

How to create multiple "has_many through" associations through one form?

I'm building a martial arts related database, currently I have the following associations set up:
Student has_and_belongs_to_many :styles
Style has_many :ranks
Student has_many :ranks, through: :gradings (and vice versa)
I'm generating a form as follows, depending on the student's styles:
So the headings are generated by the Style model (Tai Chi, Karate...), then their rankings listed below (taken from the Rank model), and the "Dojo" and "Date" fields should belong to the Grading model once created.
The question: I know how to build a form that creates one association (or one association + its children), but how do I build a form that creates multiple associations at once?
Also, what would be a clean way to implement the following:
Only lines which are ticked become associations
Dojo and date must be filled in for ticked lines to save successfully
If a line is unticked it will destroy any previously created associations
This is what I've currently implemented to retrieve the correct records:
class GradingsController < ApplicationController
before_filter :authenticate_sensei!
def index
#student = Student.includes(:styles).find(params[:student_id])
#ranks = Rank.for_student_styles(#student)
split_ranks_by_style
end
private
def split_ranks_by_style
#karate = #ranks.select_style("Karate")
#tai_chi = #ranks.select_style("Tai Chi")
#weaponry = #ranks.select_style("Weaponry")
end
end
# Rank model
def self.for_student_styles(student)
includes(:style).where("styles.id in (?)", student.styles.map(&:id))
end
def self.select_style(style)
all.map { |r| r if r.style.name == style }.compact
end
Complicated forms like this are best handled in a service object initiated in the primary resource's create or update action. This allows you to easily find where the logic is happening afterwards. In this case it looks like you can kick off your service object in your GradingsController. I also prefer formatting a lot of the data in the markup, to make the handling easier in the service object. This can be done a'la rails, by passing a name like "grade[style]" and "grade[rank]". This will format your params coming in as a convenient hash: {grade: {style: "karate", rank: "3"}}. That hash can be passed to your service object to be parsed through.
Without really grasping the full extent of your specific requirements, let's put together an example form:
<%= form_for :grading, url: gradings_path do |f| %>
<h1><%= #rank.name %></h1>
<%- #grades.each do |grade| %>
<div>
<%= hidden_field_tag "grade[#{grade.id}][id]", grade.id %>
<%= check_box_tag "grade[#{grade.id}][active]" %>
...
<%= text_field_tag "grade[#{grade.id}][date]" %>
</div>
<%- end %>
<%= submit_tag %>
<%- end %>
With a form like this, you get your params coming into the controller looking something like this:
"grade"=>{
"1"=>{"id"=>"1", "active"=>"1", "date"=>"2013-06-21"},
"3"=>{"id"=>"3", "date"=>"2013-07-01"}
}
Nicely formatted for us to hand off to our service object. Keeping our controller nice and clean:
class GradingsController < ApplicationController
def index
# ...
end
def create
builder = GradeBuilder.new(current_user, params['grade'])
if builder.run
redirect_to gradings_path
else
flash[:error] = 'Something went wrong!' # maybe even builder.error_message
render :action => :index
end
end
end
So now we just need to put any custom logic into our builder, I'd probably recommend just making a simple ruby class in your /lib directory. It could look something like this:
class GradeBuilder
attr_reader :data, :user
def self.initialize(user, params={})
#user = user
#data = params.values.select{|param| param['active'].present? }
end
def run
grades = data.each{|entry| build_grade(entry)}
return false if grades.empty?
end
private
def build_grade(entry)
grade = Grade.find(entry['id'])
rank = grade.rankings.create(student_id: user, date: entry['date'])
end
end
There will obviously need a lot more work to pass all the specific data you need from the form, and extra logic in the GradeBuilder to handle edge cases, but this will give you a framework to handle this problem in a maintainable and extensible way.

Rails upload CSV file with header

I am using Ruby 1.9.2 with Rails 3.2.1.
I would like to create a view to upload a CSV or tab delimited file, and displays the contents of the file on the same page using a table or pagination display, then process that data in JavaScript.
How can I do this? Please walk me through any code samples you have, I am a total noob in Ruby also.
First, write a view to upload your file. You can use Paperclip for this.
Assuming you have a resource Csv, your upload form could look like this:
<%= form_for #csv, :url => csv_path, :html => { :multipart => true } do |form| %>
<%= form.file_field :attachment %>
<% end %>
Your model:
class Csv < ActiveRecord::Base
attr_accessible :attachment
has_attached_file :attachment
end
Your controller actions:
def create
#csv = Csv.create( params[:csv] )
# your save and redirect code here
end
def show
#csv = Csv.find(params[:id])
end
Having that, you can use something like this in your view:
CSV.foreach(#csv.attachment.path) do |row|
# use the row here to generate html table rows
end
Note: this is just a general idea of how this can be done and you need to have the the resource added to your routes, Paperclip gem installed and configured, etc - read the doc on how to do all that.
Just use a nice Ruby gem for parsing CSV files. This should point you in the right direction. http://fastercsv.rubyforge.org/

Resources