save items in an array to active records - ruby-on-rails

I have an array that looks like this:
[{"id"=>"2", "reply"=>"ok"}, {"id"=>"3", "reply"=>"ok"}, {"id"=>"4", "reply"=>"ok"}, {"id"=>"5"}, {"id"=>"6", "reply"=>"2"}]
now I'm trying to save it like this:
current_user.answers.transaction do
success = params[:answers].map(&:save)
unless sucess.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
but that results in
undefined method `save' for {"id"=>"2", "reply"=>"ok"}:ActionController::Parameters
does anybody know how I can save each item?
class Answer < ActiveRecord::Base
belongs_to :user
belongs_to :question
has_many :comments
validates :reply, :question_id, :week_number, presence: true
end

Yeah, just send it a block... so you can try:
current_user.answers.transaction do
success = params[:answers].map do |hash|
object = Model.new(hash)
object.save
end
unless success.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
Realize that Model needs to be the name of the model you're trying to instantiate objects with.
but, that's only if you're making new objects, which generally won't be the case of you've set the id attribute already
current_user.answers.transaction do
success = params[:answers].map do |hash|
object = Model.find(hash["id"])
object.update_attributes(hash)
end
unless success.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
Hope this helps

Related

How to fix Error ActiveRecord::RecordNotSaved when calling ActiveRecord#find_or_create_by

I am working on a website and whenever someone forgets a null field in the form I get an error saying
You cannot call create unless the parent is saved
This is the trace:
Application Trace
app/views/technicians/offer_comments/_offer_comment.html.slim:1:in `_app_views_technicians_offer_comments__offer_comment_html_slim__1148413763742950523_70319840794240'
app/views/offer_comments/index.html.slim:2:in `_app_views_offer_comments_index_html_slim___917297770217289302_70319839456700'
app/views/shared/offers/_comments.html.slim:8:in `_app_views_shared_offers__comments_html_slim__3779418887197040636_70319839163900'
app/views/technicians/auctions/show.html.slim:98:in `block in _app_views_technicians_auctions_show_html_slim___1454306351028108654_70319829646100'
app/helpers/application_helper.rb:14:in `rescue in cache'
app/helpers/application_helper.rb:6:in `cache'
app/views/technicians/auctions/show.html.slim:1:in `_app_views_technicians_auctions_show_html_slim___1454306351028108654_70319829646100'
app/controllers/technicians/offers_controller.rb:54:in `update'
The error appears in the first line of this html.slim view:
- offer_comment.read_receipts.find_or_create_by user: current_user
.comment id="offer-#{offer_comment.offer_id}-comment-#{offer_comment.id}"
.by
- if offer_comment.user == current_user
= t ".you"
- else
= t ".not_you"
= " - "
= t '.date', date: time_ago_in_words(offer_comment.created_at)
.content
= raw markdown offer_comment.content
The interesting part is that this error only occurs when I call another object, offers, in the main view in which the previous code is rendered: show.html.slim (last line)
ul#customer-auction-tabs.tabs.clean.collapse(data-tabs)
a#auctions-tabs-chevron href="#"
i#auctions-tabs-chevron-icon.fas.fa-chevron-up
li.tabs-title class=chat_active_class
a#chat-tab href="#chat" aria-selected="true"= t '.tabs.chat'
li.tabs-title class=offer_active_class
a#offers-tab href="#offers"= t '.tabs.offer'
- if comments_count > 0
li.tabs-title class=comments_active_class
a#comments-tab href="#comments"= t '.tabs.comments'
li.tabs-title class=other_active_class
a#other-tab href="#other"= t '.tabs.other'
.auctions.tabs-content data-tabs-content="customer-auction-tabs"
#chat.tabs-panel class=chat_active_class
= render partial: "shared/auctions/chat", locals: { auction: auction }
#offers.tabs-panel class=offer_active_class
= render partial: "shared/offers/new", locals: { offer: offer }
#comments.tabs-panel class=comments_active_class
= render partial: 'shared/offers/comments', locals: { offer: offer }
#other.tabs-panel class=other_active_class
- if auction.offers.count.zero?
= t "ingen andre bud endnu"
= render "shared/offers/other"
.offers= render offers
I don't understand how this works because find_or_create_by is apparently supposed to work even if the object hasn't been saved.
Can someone help me solve this issue, and preferably avoid using logic like find_or_create_by in the view at all?
Here is part of the Offer model:
class Offer < ApplicationRecord
has_paper_trail
belongs_to :auction, -> { with_deleted }, counter_cache: true, touch: true
belongs_to :technician, counter_cache: true, foreign_key: :technician_id
has_one :settings, through: :technician
has_many :comments, -> { order(created_at: :asc) }, class_name: "OfferComment"
has_one :review, as: :target
delegate :rating, to: :review, allow_nil: true
delegate :rating_date, to: :review, allow_nil: true
delegate :rating_comment, to: :review, allow_nil: true
validates :description, presence: true
validates :cents, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :amount_validity
scope :not_by, ->(technician) { where.not(technician: technician) }
Here is also the controller update action that gets called when updating the form with a null field:
class Technicians::OffersController < Technicians::ApplicationController
rescue_from ActiveRecord::RecordNotFound do
render "technicians/auctions/lost", status: 404
end
def update
offer.attributes = offer_params
changed = offer.changed?
if offer.save
OfferUpdatedWorker.perform_async offer.id if changed
flash[:info] = t(".success")
redirect_to [:technicians, auction]
else
flash.now[:error] = t(".failure")
render "technicians/auctions/show",
locals: { auction: auction, offer: offer },
status: 400
end
end
Another important file to note is the auction controller that originally calls "technicians/auctions/show"
def show
render(:lost) && return if lost?
render :show, locals: {
offers: sorted_o,
auction: auction,
#other: other,
offer: offer,
} if stale? **cache_options(auction.id, auction.updated_at)
end
private
#=begin
def sorted_o
#sorted_o ||= begin
field = (%w[cheapest closest guarantee] & [params[:sort]])[0].presence || "cheapest"
case field
when "closest"
auction
.offers
.includes(:auction, :technician, :review)
.sort_by { |o| distance(o.technician, auction) }
when "guarantee"
auction
.offers
.includes(:auction, :technician, :review)
.joins(:settings)
.order("technician_settings.guarantee desc")
else
auction
.offers
.includes(:auction, :technician, :review)
.order(cents: :asc)
end
end
end
#=end
def offer
#offer ||= auction.offers.by(current_user) ||
auction.offers.new(technician: current_user)
end
It looks like you need offer_comment to be saved before a read_receipt that belongs to it can be created, which makes sense - the offer_comment doesn't have an id until it has been saved.
This might get you past the problem.
offer_comment.tap(&:save).read_receipts.find_or_create_by user: current_user
I fixed it. The cause of the problem was that when the update action fails in offers_controller.rb it calls the show view without the offers variable, this variable is somehow related to the offer_comments, but I'm not sure how/why because offer_comments is supposed to be only related to the variable offer and not offers.
However, when I checked the console there were null elements in the offer_comments so I just went on and changed the view to only show non null elements, the error stack then pointed to offers not being defined in the show view.

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

How can I lessen the verbosity of my populate method?

I wrote a form object to populate an Order, Billing, and Shipping Address objects. The populate method looks pretty verbose. Since the form fields don't correspond to Address attributes directly, I'm forced to manually assign them. For example:
shipping_address.name = params[:shipping_name]
billing_address.name = params[:billing_name]
Here's the object. Note that I snipped most address fields and validations, and some other code, for brevity. But this should give you an idea. Take note of the populate method:
class OrderForm
attr_accessor :params
delegate :email, :bill_to_shipping_address, to: :order
delegate :name, :street, to: :shipping_address, prefix: :shipping
delegate :name, :street, to: :billing_address, prefix: :billing
validates :shipping_name, presence: true
validates :billing_name, presence: true, unless: -> { bill_to_shipping_address }
def initialize(item, params = nil, customer = nil)
#item, #params, #customer = item, params, customer
end
def submit
populate
# snip
end
def order
#order ||= #item.build_order do |order|
order.customer = #customer if #customer
end
end
def shipping_address
#shipping_address ||= order.build_shipping_address
end
def billing_address
#billing_address ||= order.build_billing_address
end
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
if order.bill_to_shipping_address?
billing_address.name = params[:shipping_name]
billing_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
else
billing_address.name = params[:billing_name]
billing_address.street = params[:billing_street]
# Repeat for city, state, post, code, etc...
end
end
end
Here's the controller code:
def new
#order_form = OrderForm.new(#item)
end
def create
#order_form = OrderForm.new(#item, params[:order], current_user)
if #order_form.submit
# handle payment
else
render 'new'
end
end
Noe I am not interested in accepts_nested_attributes_for, which presents several problems, hence why I wrote the form object.
def populate
order.email = params[:email]
shipping_params = %i[shipping_name shipping_street]
billing_params = order.bill_to_shipping_address? ?
shipping_params : %i[billing_name billing_street]
[[shipping_address, shipping_params], [billing_address, billing_params]]
.each{|a, p|
a.name, a.street = params.at(*p)
}
end
How about
class Order < ActiveRecord::Base
has_one :shipping_address, class_name: 'Address'
has_one :billing_address, class_name: 'Address'
accepts_nested_attributes_for :shipping_address, :billing_address
before_save :clone_shipping_address_into_billing_address, if: [check if billing address is blank]
Then when you set up the form, you can have fields_for the two Address objects, and side step the populate method entirely.
A possible fix would be to use a variable for retrieving those matching params, like so:
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# etc...
#set a default state
shipping_or_billing = "shipping_"
#or use a ternary here...
shipping_or_billing = "billing_" if order.bill_to_shipping_address?
billing_address.name = params["shipping_or_billing" + "name"]
billing_address.street = params["shipping_or_billing" + "street"]
...
end
Your address classes should probably have a method that would set the values for all the address properties from a hash that it would receive as an argument.
That way your populate method would only check for order.bill_to_shipping_address? and them pass the correct dictionary to the method I'm suggesting.
That method on the other hand, would just assign the values from the hash to the correct properties, without the need for a conditional check.

How to duplicate a group of "tasks" that belong to the same model

I'm in rails 3.0 and I'm working on a "project management" app. I'd like to duplicate an Item, which in my case is the "project", and at the same time, duplicate all tasks that belong to that item.
I stuffed my Item model with code I found here: http://www.redmine.org/projects/redmine/repository/revisions/2704/diff/trunk/app/models/project.rb, which seems to do what I want, but I can't make it work for me.
I'd like any help you can offer--general or specific! thanks!
class Task < ActiveRecord::Base
belongs_to :department
belongs_to :item
belongs_to :customer
end
class Item < ActiveRecord::Base
belongs_to :customer
has_many :tasks
def copy(item)
item = item.is_a?(Item) ? item : Item.find(item)
Item.transaction do
# Tasks
item.tasks.each do |task|
new_task = Task.new
new_task.copy_from(task)
self.tasks << new_task
end
self.save
Hook.call_hook(:model_item_copy_before_save, :source_item => item, :destination_item => self)
end
end
def self.copy_from(item)
begin
item = item.is_a?(Item) ? item : Item.find(item)
if item
# clear unique attributes
attributes = item.attributes.dup.except('id')
copy = Item.new(attributes)
copy.enabled_modules = item.enabled_modules
copy.trackers = item.trackers
copy.custom_values = item.custom_values.collect {|v| v.clone}
return copy
else
return nil
end
rescue ActiveRecord::RecordNotFound
return nil
end
end
Another thing--what is the Hook.call_hook...? I can't find any references to that on the web
Look into ActiveResource::Base#clone.
It should probably work something like:
#project = #project2.clone
#project.tasks << #project2.tasks.map(&:clone)
#project.save
EDIT:
In the context of your model you could just have:
def self.copy(item)
newitem = item.clone
newitem.tasks << item.tasks.map(&:clone)
return newitem
end
Then in your controller:
#project = Project.copy(#project_to_copy)
#project.save

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