Update record if exists with roo gem - ruby-on-rails

I have an import working correctly from a Spreadsheet using Roo gem.
The problem is every time I call the rake task, new records are created.
I want to update_attributes of the records in case the record exists.
Is there any way to approach this? I've tried this with no luck:
namespace :import do
desc "Import data from spreadsheet" # update this line
task data: :environment do
data = Roo::Spreadsheet.open('lib/t3.xlsx') # open spreadsheet
headers = data.row(1) # get header row
data.each_with_index do |row, idx|
next if idx == 0 # skip header
# create hash from headers and cells
product_data = Hash[[headers, row].transpose]
product = Product.new(product_data)
puts "Guardando Producto #{product.name}"
if product?
product.update_attributes
else
product.save!
end
rescue ActiveRecord::RecordInvalid => invalid
puts invalid.record.errors
end
end
end

if product? will never return false. You're testing whether the variable contains a falsy value (nil/false) or any other value. After calling product = Product.new, the value stored in product can never be nil or false.
What you want is to first find, and if not found, new, and then update_attributes on the resulting object:
product = Product.find_by(product_data.name) || Product.new
product.update_attributes(product_data)

Related

Modifying ActiveRecord models before preventing deletion

Some records in my application have a DOI assigned to them and in that case they should not be deleted. Instead, they should have their description changed and be flagged when a user triggers their deletion. A way to do this, I thought, would be as follows in the relevant model:
before_destroy :destroy_validation
private
def destroy_validation
if metadata['doi'].blank?
# Delete as normal...
nil
else
# This is a JSON field.
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
raise ActiveRecord::RecordNotDestroyed, 'Records with DOIs cannot be deleted'
end
end
This does indeed prevent deletion, but the record appears unchanged afterwards rather than having a modified description. Here's an example of a test:
test "records with dois are not deleted" do
record = Record.new(metadata: metadata)
record.metadata['doi'] = 'this_is_a_doi'
assert record.save
assert_raises(ActiveRecord::RecordNotDestroyed) { record.destroy! }
assert Record.exists?(record.id)
modified_record = Record.find(record.id)
puts "#{record.description}" # This is correctly modified as per the callback code.
puts "#{modified_record.description}" # This is the same as when the record was created.
end
I can only guess that Rails is rolling back the update_column due to an exception having been raised, though I may be mistaken. Is there anything I can do to prevent this?
save and destroy are automatically wrapped in a transaction
https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
So destroy fails, transactions is rolled back and you can't see updated column in tests.
You could try with after_rollback callback https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback
or do record.destroy check for record.errors, if found update record with method manually record.update_doi if record.errors.any?.
before_destroy :destroyable?
...
def destroyable?
unless metadata['doi'].blank?
errors.add('Doi is not empty.')
throw :abort
end
end
def update_doi
modified_metadata = Marshal.load(Marshal.dump(metadata))
description = "Record does not exist anymore: #{name}. The record with identifier content #{doi} was invalid."
modified_metadata['description'] = description
modified_metadata['tombstone'] = true
update_column :metadata, modified_metadata
end
Tip: use record.reload instead of Record.find(record.id).

How to save each value from an array to database in Rails?

I would like to store each value from an array.
For example the form sends me this data:
"attendance"=>{"event_id"=>"6", "member_id"=>["16", "28", "26"]}
I'd like the database to store the data as:
INSERT INTO "attendances" ("event_id", "member_id") VALUES ("6", "16")
INSERT INTO "attendances" ("event_id", "member_id") VALUES ("6", "28")
INSERT INTO "attendances" ("event_id", "member_id") VALUES ("6", "26")
I've tried to use the usual way of inserting data in Rails, but it failed because the member_ids didn't get passed (I've tried to print the member_ids after the Attendance.new(attendance_params)):
def create
#attendance = Attendance.new(attendance_params)
# puts #attendance[:event_id]
# puts #attendance[:member_id] -> Nothing showed up here.
if #attendance.save
flash[:success] = "Successfully created"
redirect_to new_attendance_path
else
#error_msg = #attendance.errors.full_messages
flash[:error] = #error_msg # Prints ["Member must exist"]
redirect_to new_attendance_path
end
end
I've also tried creating a new function in the model to change the Attendance.new but it'll return
NoMethodError - undefined method `new_each' for #<Class:0x000000000d1e7280>: app/controllers/attendances_controller.rb:17:in `create'
This is my current model:
class Attendance < ApplicationRecord
belongs_to :event
belongs_to :member
# def new_each(attendance)
# attendance_event = attenance[:event_id]
# attendance_members = attendance[:member_id]
# I tried to iterate and save each data here.
# end
end
So, how do I save each value from an array input (from the form) and save it to database?
Any answers and comment will be very appreciated.
You are trying to add attendance in bulk, For that you can do 2 things,
use gem bulk_insert, it's very easy to use here is the link https://github.com/jamis/bulk_insert
you need to iterate through all the members and events from params and create a record for each. though it's a very dirty way.
sample code would be like this. you can modify it as per your need.
errors = {}
attendance_params[:member_id].each do |member_id|
attendence = Attendence.new(event_id:attendance_params[:event_id], member_id: member_id)
errors[member_id] = "Could not save attendance, error =
attendance.errors.full_messages" unless
attendence.save
end

Get row of inserted id in rails transaction with postgres

I have the following code:
#which receives Group objects and saves them in a transaction
def saveGroup(group_buffer)
self.transaction do
output = group_buffer.each(&:save)
if(!output)
return false
break
elsif( #I want the inserted row id as they are inserted)
#put inserted row id in array
end
end
return #the_array
end
Is this possible? Basically, what I want is to obtain the inserted row id in a transaction as the objects are saved and push it into an array. What is the best way to do this? Thank you very much...
Is this what you are looking for? if any instance will not save, the transaction will roll back, so you don't have to brake it manually.
def saveGroup(group_buffer)
ids = []
self.transaction do
group_buffer.each do |i|
i.save
ids << i.id
end
end
if ids.empty?
false
else
ids
end
end

Rails Parse CSV with empty cells and correctly handle exceptions

I am trying to allow users to upload csv files, however I am having some issues trying to figure out how to handle errors from the file itself.
My controller method:
def create
Product.import_csv(file)
redirect_to products_path, :flash => { :notice => "Items Added!" }
end
My model method to import the file:
def self.import_csv(file)
csv = CSV.read(file.path), :headers => true)
csv.each do |row|
item_id = row[0]
start_date = Date.parse(row[1])
order_date = Date.parse(row[2])
new_rec = where(item_id:item_id, order_date:order_date).first_or_initialize
new_rec.save!
end
end
All this works well when the file is properly formatted, however Im confused as to how to handle exceptions. Once such exception is when start_date or order_date are missing; I get an no implicit conversion of nil into String because I'm attempting to parse the date of an empty cell. Even though I have validations in my model for presence, they only get fired on the save action.
I don't want to silently ignore these errors with a rescue block, but instead redirect back, and notify the user.
How can I handle such exceptions, so that the jobs fails, and the user gets notified of the error, not specifically solely the error given above, but including errors we can't account for? Another example would be a empty line or something we cant even account for. How can I handle such errors, and notify the user with, for example, a generic "Bad data" message?
Should I handle this in my model or my controller?
If you have presence validations for the dates, you can simply assign them only if they exist, and let your model do the rest:
def self.import_csv(file)
csv = CSV.read(file.path), :headers => true)
csv.each do |row|
item_id = row[0]
start_date = row[1].nil? ? nil : Date.parse(row[1])
order_date = row[2].nil? ? nil : Date.parse(row[2])
new_rec = where(item_id:item_id, order_date:order_date).first_or_initialize
new_rec.save!
end
end

Using ruby 1.9.2 and rails 3, how do I collect activerecord errors to display later? Or better yet, hold off on the commit until ready?

In a single operation, I am inserting multiple rows into a table. It is possible for one or more of those rows to cause an ActiveRecord::RecordInvalid issue. When this happens, I would like to be able to back out all the transactions for this particular operation and have the user fix the data before proceeding. What is the best way to go about doing this?
Right now, if it fails on the 2nd row of data, the 1st one has already been committed to the database so the user will not know if they should reload that first row or not. I can just inform the user how many rows succeeded and they can know to fix and reload only from that part on, but it would be better for me if I could just undo everything and have the user start over after they fix their data.
FYI, the user initially loads a CSV file into a table that contains on row for every row*column of their csv file and I import from that import_table.
This is a piece of my method in the controller:
def process_import
#import = ImportTable.find(params[:id])
#cells = #import.import_cells
#new_donors = Array.new
#existing_donors = Array.new
class_name = 'Donor'
klass = ActiveRecord.const_get(class_name) # get access to class
#loop through rows
0.upto(#import.row_count - 1) do |row_index|
donor = {}
donation = {}
record_status = 'new'
row = #cells.select { |cell| cell.row_index == row_index }
#loop through columns
0.upto(#import.column_count - 1) do |column_index|
contents = row.select { |cell| cell.column_index == column_index}[0].contents
case column_index
when 0 then record_status = contents
when 1 then donor["id"] = contents
when 2 then donor["prefix1"] = contents
when 3 then donor["first_name1"] = contents
when 4 then donor["middle_name1"] = contents
...
end #case
end #columns
unless #donor = Donor.find_by_id(donor["id"])
donor.delete("id")
#donor = klass.find_or_initialize_by_company_and_prefix1_and_first_name1_and_last_name1_and_address1(donor)
end
#donor.new_record? ? #new_donors.push(#donor) : #existing_donors.push(#donor)
#donor.save!
if !donation["amount"].blank?
#donation = #donor.donations.build(donation)
#donation.save!
end
end #rows
#import.processed_date = Time.now
#import.save
end
I would just have the user resubmit the entire data
Wrap the import process in a transaction. If an error occurs then you can back out of the entire commit by raising an ActiveRecord::Rollback
klass.transaction do
# loop through rows
# You might want to call #donor.save rather than #donor.save! so that you do not raise errors, yet still have invalid records.
raise ActiveRecord::Rollback if (#new_donors+#existing_donors).any?{|donor| !donor.valid? }
end

Resources