Rails Import data from csv with array many to many - ruby-on-rails

Good morning! My english is not the best.
I'm trying to import some data from csv file using this model.
class Recibo < ActiveRecord::Base
attr_accessible :id,
:caja_id,
:doctor_id,
:numero_recibo,
:paciente,
:total,
:total_porcentaje_doctor,
:total_porcentaje_clinica,
:total_porcentaje_laboratorio,
:servicio_ids,
:created_at,
:updated_at
belongs_to :caja
belongs_to :doctor
has_many :atencions
has_many :servicios, :through => :atencions
before_save do
servicio_by_id = Servicio.where(:id => servicio_ids)
self.total = servicio_by_id.sum(&:precio)
self.total_porcentaje_doctor = servicio_by_id.sum ('porcentaje_doctor / 100.0 * precio')
self.total_porcentaje_clinica = servicio_by_id.sum ('porcentaje_clinica / 100.0 * precio')
self.total_porcentaje_laboratorio = servicio_by_id.sum ('porcentaje_laboratorio / 100.0 * precio')
end
def self.to_csv
CSV.generate do |csv|
csv << ["id", "caja_id", "doctor_id", "numero_recibo", "paciente", "total", "total_porcentaje_laboratorio",
"total_porcentaje_clinica", "total_porcentaje_doctor", "created_at", "updated_at", "servicio_ids" ]
all.each do |recibo|
recibo.atencions.map(&:servicio_id)
csv << [recibo.id, recibo.caja_id, recibo.doctor_id, recibo.numero_recibo,
recibo.paciente, recibo.total, recibo.total_porcentaje_laboratorio, recibo.total_porcentaje_clinica,
recibo.total_porcentaje_doctor, recibo.created_at, recibo.updated_at, recibo.servicio_ids]
end
end
end
def self.import(file)
CSV.foreach(file.path, headers: true) do |row|
recibo = find_by_id(row["id"]) || new
recibo.attributes = row.to_hash.slice(*accessible_attributes)
recibo.save!
end
end
end
my csv file contain data like this:
id,caja_id,doctor_id,numero_recibo,paciente,total,total_porcentaje_laboratorio,total_porcentaje_clinica,total_porcentaje_doctor,created_at,updated_at,servicio_ids
1,2,3,,Nombre,8,0,4,4,2014-04-21 15:45:29 -0500,2014-05-27 18:58:54 -0500,[1]
2,2,1,,Nombre2,11,0,5.5,5.5,2014-04-21 16:38:32 -0500,2014-05-27 19:28:20 -0500,[1, 8]
The self.import(file) suppose to add the records servicio_ids in the table atencion but it dosen't. I don't know what to do.
Thanks for everything!

When generating your .csv file, instead of:
CSV.generate do |csv|
do:
CSV.generate(force_quotes: true) do |csv|
By default CSV adds commas between the values, which messes up the parsing in your case, because of the commas inside the array elements.

Related

Unable to save data to a Database importing a CSV file in Ruby on Rails

I have been trying to import a .csv file on my Ruby on Rails application in order to store the data about a Peak, but failed using different methods. The application redirects to the root_url but the database remains empty. The program seems to work, at least it does not provide errors, but no data is imported into the database. This how my peaks_controller looks like:
class PeaksController < ApplicationController
def show
#peak = Peak.find(params[:id])
end
def index
#peaks = Peak.all
end
def import
Peak.import(params[:file])
redirect_to root_url, notice: "Peaks imported."
end
def new
#peak = Peak.new
end
def create
#peak = Peak.new(peak_params)
if #peak.save
redirect_to #peak
else
render 'new'
end
end
def destroy
end
private
def peak_params
params.require(:peak).permit(:name, :altitude, :prominence, :isolation, :key_col, :source, :accessibility, :land_use, :hazard, :longitude, :latitude)
end
end
This is my peak.rb class:
class Peak < ApplicationRecord
validates :altitude, presence: true
validates :prominence, presence: true
validates :isolation, presence: true
validates :key_col, presence: true
validates :source, presence: true
validates :accessibility, presence: true
validates :land_use, presence: true
validates :longitude, presence: true
validates :latitude, presence: true
#non funziona
def self.import(file)
csv = CSV.parse(File.read(file), :headers => true)
csv.each do |row|
p = Peak.new
p.id = row['id']
p.name = row['Name']
p.altitude = row['Altitude']
p.prominence = row['Prominence']
p.isolation = row['Isolation']
p.key_col = row['Key_col']
p.source = row['Source']
p.accessibility = row['Accessibility']
p.land_use = row['Land_use']
p.hazard = row['Hazard']
p.longitude = row['x']
p.latitude = row['y']
p.save
end
end
end
That just does not return anything, so it does not import anything into my database, I have also tried the following import method:
def self.import(file)
csv = CSV.parse(File.read(file), :headers => false)
csv.each do |row|
Peak.create!(row.to_h)
end
end
This is how my database looks like:
class CreatePeaks < ActiveRecord::Migration[6.0]
def change
create_table :peaks do |t|
t.string :name
t.decimal :altitude
t.decimal :prominence
t.decimal :isolation
t.decimal :key_col
t.string :source
t.decimal :accessibility
t.string :land_use
t.string :hazard
t.decimal :longitude
t.decimal :latitude
end
end
end
[And this are the headerds of the .csv file][:1]
I have inclunded "require 'csv'" on my application.rb so the application reads a csv file.
I have also tried to remove the 'id' column from the file and tried with the "row.to_h" but it did not change anything, still can't import the values onto the database.
Do you have any suggestions?
The hard part when doing a mass import is error handling - of which you're doing none. So all those calls to save the records could be failing and you're none the wiser.
You basically have two options:
1. Strict
Wrap the import in a transaction and roll back if any of the records are invalid. This avoids leaving the job half done.
class Peak < ApplicationRecord
# ...
def self.import(file)
csv = CSV.parse(File.read(file), :headers => true)
transaction do
csv.map do |row|
create!(
id: row['id'],
name: row['Name'],
altitude: row['Altitude'],
prominence: row['Prominence'],
isolation: row['Isolation'],
key_col: row['Key_col'],
source: row['Source'],
accessibility: row['Accessibility'],
land_use: row['Land_use'],
hazard: row['Hazard'],
longitude: row['x'],
latitude: row['y']
)
end
end
end
end
class PeaksController < ApplicationController
def import
begin
#peaks = Peak.import(params[:file])
redirect_to root_url, notice: "Peaks imported."
rescue ActiveRecord::RecordInvalid
redirect_to 'somewhere/else', error: "Import failed."
end
end
end
2. Lax
In some scenarios you might want to do lax processing and just keep on going even if some records fail to import. This can be combined with a form that lets the user correct the invalid values.
class Peak < ApplicationRecord
# ...
def self.import(file)
csv = CSV.parse(File.read(file), :headers => true)
peaks = csv.map do |row|
create_with(
name: row['Name'],
altitude: row['Altitude'],
prominence: row['Prominence'],
isolation: row['Isolation'],
key_col: row['Key_col'],
source: row['Source'],
accessibility: row['Accessibility'],
land_use: row['Land_use'],
hazard: row['Hazard'],
longitude: row['x'],
latitude: row['y']
).find_or_create_by(id: row['id'])
end
end
end
class PeaksController < ApplicationController
def import
#peaks = Peak.import(params[:file])
#invalid_peaks = #peaks.reject {|p| p.persisted? }
if #invalid_peaks.none?
redirect_to root_url, notice: "Peaks imported."
else
flash.now[:error] = "import failed"
render 'some_kind_of_form'
end
end
end

Error handling of csv upload in rails

I have this import method in my active record which I use to import the csv file. I want to know how to do the error handling of this in the active record.
class SheetEntry < ActiveRecord::Base
unloadable
belongs_to :user
belongs_to :project
belongs_to :task
validate :project_and_task_should_be_active
def self.import(csv_file)
attributes = [:user_id, :project_id, :task_id, :date, :time_spent, :comment]
errors=[]
output = {}
i=0
CSV.foreach(csv_file, headers: true, converters: :date).with_index do |row,j|
entry_hash= row.to_hash
entry_hash['Project'] = SheetProject.where("name= ?" , entry_hash['Project']).pluck(:id)
entry_hash['Task'] = SheetTask.where("name= ?" , entry_hash['Task']).pluck(:id)
entry_hash['Date'] = Time.strptime(entry_hash['Date'], '%m/%d/%Y').strftime('%Y-%m-%d')
entry_hash['Time (Hours)'] = entry_hash['Time (Hours)'].to_f
firstname = entry_hash['User'].split(" ")[0]
lastname = entry_hash['User'].split(" ")[1]
entry_hash['User'] = User.where("firstname=? AND lastname=?",firstname,lastname).pluck(:id)
entry_hash.each do |key,value|
if value.class == Array
output[attributes[i]] = value.first.to_i
else
output[attributes[i]] = value
end
i += 1
end
entry=SheetEntry.new(output)
entry.editing_user = User.current
entry.save!
end
end
def project_and_task_should_be_active
errors.add(:sheet_project, "should be active") unless sheet_project.active?
errors.add(:sheet_task, "should be active") if sheet_task && !sheet_task.active?
end
end
I want to know how to show the error if there is a nil object returned for either entry_hash['Project'] or entry_hash['Task'] or for any of the fields in the csv.
For example: If the user had entered the wrong project or wrong task or wrong date. I want the error to be shown along with the line no and stop the uploading of the csv. Can someone help?
You can use begin and rescue statements to handle errors in any ruby classes.
You can use the rescue block to return the Exception e back to the caller.
However, you cannot call errors.add method to add error because #errors is an instance method which is not accessible inside class method self.import.
def self.import(csv_file)
begin
attributes = [:user_id, :project_id, :task_id, :date, :time_spent, :comment]
errors=[]
output = {}
i=0
CSV.foreach(csv_file, headers: true, converters: :date).with_index do |row,j|
...
end
rescue Exception => e
return "Error: #{e}"
end
end

Rails CSV Import switch string for foreign key

I have set up a CSV import for my Ruby App. Everything works fairly well but I'm running into problems when I try to upload a string that then searches for an id to input as the foreign key in the hash. My model for the CSV looks as follows:
class Player < ActiveRecord::Base
belongs_to :team
validates :team_id, presence: true
def self.import(file)
CSV.foreach(file.path, headers: true, :header_converters => :symbol) do |row|
player_hash = row.to_hash
teamname = player_hash[:team]
teamhash = Team.where(:name => teamname).first
hashid = teamhash.id
player_newhash = player_hash.reject!{ |k| k == :team}
player_newhash[:team_id] = hashid
end
Player.create! (player_newhash)
end
end
I'm sure this is where the problem lies. When trying to execute I get the error:
undefined local variable or method `player_newhash' for #
Any help would be greatly appreciated.
player_newhash is a local variable inside your foreach loop - so your create would need to be in there as well:
class Player < ActiveRecord::Base
belongs_to :team
validates :team_id, presence: true
def self.import(file)
CSV.foreach(file.path, headers: true, :header_converters => :symbol) do |row|
player_hash = row.to_hash
teamname = player_hash[:team]
teamhash = Team.where(:name => teamname).first
hashid = teamhash.id
player_newhash = player_hash.reject!{ |k| k == :team}
player_newhash[:team_id] = hashid
Player.create!(player_newhash)
end
end
end
BTW - I got that answer by refactoring for a second to find out what was going on... in case it's helpful my version looked like this:
class Player < ActiveRecord::Base
belongs_to :team
validates :team_id, presence: true
def self.import(file)
CSV.foreach(file.path, headers: true, :header_converters => :symbol) do |row|
player_hash = row.to_hash
player_newhash = player_hash.reject!{ |k| k == :team}
player_newhash[:team_id] = find_team(player_hash[:team]).id
Player.create!(player_newhash)
end
end
private
def find_team(team_name)
Team.where(name: team_name).first
end
end

Rails 4 - exporting routes to CSV

I'm developing an ecommerce app and I have a csv export feature which exports all product details like name, price, etc. Each product is in one row with a column for each product attribute. I want to add a column to the file which will contain the url of each product. The reason I want this is so I can use this as a product feed that can be submitted to various shopping sites.
Here is my export code in the controller. How do I add a column called route to this? I don't have a route column in the model.
#controller
def productlist
#listings = Listing.all
respond_to do |format|
format.html
format.csv { send_data #listings.to_csv(#listings) }
end
end
#model
def self.to_csv(listings)
wanted_columns = [:sku, :name, :designer_or_brand, :description, :price, :saleprice, :inventory, :category]
CSV.generate do |csv|
csv << ['Product_ID', 'Product_title', 'Designer_or_Brand', 'Description', 'Price', 'SalePrice', 'Quantity_in_stock', 'Category'] + [:Image, :Image2, :Image3, :Image4]
listings.each do |listing|
attrs = listing.attributes.with_indifferent_access.values_at(*wanted_columns)
attrs.push(listing.image.url, listing.image2.try(:url), listing.image3.try(:url), listing.image4.try(:url))
csv << attrs
end
end
end
def self.to_csv(listings)
wanted_columns = [:sku, :name, :designer_or_brand, :description, :price,
:saleprice, :inventory, :category]
header = %w(Product_ID Product_title Designer_or_Brand Description Price
SalePrice Quantity_in_stock Category Image Image2 Image3 Image4 ProductUrl)
CSV.generate do |csv|
csv << header
listings.each do |listing|
attrs = listing.attributes.with_indifferent_access.values_at(*wanted_columns)
<< listing.image.url << listing.image2.try(:url)
<< listing.image3.try(:url) << listing.image4.try(:url)
<< Rails.application.routes.url_helpers.product_url(listing.Product_ID)
csv << attrs
end
end
end
Actually the only difference is last item of array: Rails.application.routes.url_helpers.product_url(listing.Product_ID), where product_url is your route to product#show

Bulk insert using one model

I'm trying to create a form using textarea and a submit button that will allow users to do bulk insert. For example, the input would look like this:
0001;MR A
0002;MR B
The result would look like this:
mysql> select * from members;
+------+------+------+
| id | no | name |
+------+------+------+
| 1 | 0001 | MR A |
+------+------+------+
| 2 | 0002 | MR B |
+------+------+------+
I'm very new to Rails and I'm not sure on how to proceed with this one. Should I use attr_accessor? How do I handle failed validations in the form view? Is there any example? Thanks in advance.
Update
Based on MissingHandle's comment, I created a Scaffold and replace the Model's code with this:
class MemberBulk < ActiveRecord::Base
attr_accessor :member
def self.columns
#columsn ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :data, :text
validates :data, :create_members, :presence => true
def create_members
rows = self.data.split("\r\n")
#member = Array.new
rows.each_with_index { |row, i|
rows[i] = row.strip
cols = row.split(";")
p = Member.new
p.no = cols[0]
p.name = cols[1]
if p.valid?
member << p
else
p.errors.map { |k, v| errors.add(:data, "\"#{row}\" #{v}") }
end
}
end
def create_or_update
member.each { |p|
p.save
}
end
end
I know the code is far from complete, but I need to know is this the correct way to do it?
class MemberBulk < ActiveRecord::Base
#Tells Rails this is not actually tied to a database table
# or is it self.abstract_class = true
# or #abstract_class = true
# ?
abstract_class = true
# members holds array of members to be saved
# submitted_text is the data submitted in the form for a bulk update
attr_accessor :members, :submitted_text
attr_accessible :submitted_text
before_validation :build_members_from_text
def build_members_from_text
self.members = []
submitted_text.each_line("\r\n") do |member_as_text|
member_as_array = member_as_text.split(";")
self.members << Member.new(:number => member_as_array[0], :name => member_as_array[1])
end
end
def valid?
self.members.all?{ |m| m.valid? }
end
def save
self.members.all?{ |m| m.save }
end
end
class Member < ActiveRecord::Base
validates :number, :presence => true, :numericality => true
validates :name, :presence => true
end
So, in this code, members is an array that is a collection of the individual Member objects. And my thinking is that as much as possible, you want to hand off work to the Member class, as it is the class that will actually be tied to a database table, and on which you can expect standard rails model behavior. In order to accomplish this, I override two methods common to all ActiveRecord models: save and valid. A MemberBulk will only be valid if all it's members are valid and it will only count as saved if all of it's members are saved. You should probably also override the errors method to return the errors of it's underlying members, possibly with an indication of which one it is in the submitted text.
In the end I had to change from using Abstract Class to Active Model (not sure why, but it stoppped working the moment I upgrade to Rails v3.1). Here's the working code:
class MemberBulk
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :input, :data
validates :input, presence: true
def initialize(attributes = {})no
attributes.each do |name, value|
send("#{name}=", value) if respond_to?("#{name}=")
end
end
def persisted?
false
end
def save
unless self.valid?
return false
end
data = Array.new
# Check for spaces
input.strip.split("\r\n").each do |i|
if i.strip.empty?
errors.add(:input, "There shouldn't be any empty lines")
end
no, nama = i.strip.split(";")
if no.nil? or nama.nil?
errors.add(:input, "#{i} doesn't have no or name")
else
no.strip!
nama.strip!
if no.empty? or nama.empty?
errors.add(:input, "#{i} doesn't have no or name")
end
end
p = Member.new(no: no, nama: nama)
if p.valid?
data << p
else
p.errors.full_messages.each do |error|
errors.add(:input, "\"#{i}\": #{error}")
end
end
end # input.strip
if errors.empty?
if data.any?
begin
data.each do |d|
d.save
end
rescue Exception => e
raise ActiveRecord::Rollback
end
else
errors.add(:input, "No data to be processed")
return false
end
else
return false
end
end # def
end

Resources