The app finds or initializes by an attribute:
booking = Booking.where(deal_id: params["id"]).first_or_initialize
Then updates some additional attributes
if booking.update!(
guests: guests,
names: names,
time: time)
This code gives me the error
Validation failed: Deal has already been taken
The validation code is
validates_uniqueness_of :deal_id, allow_blank: true, scope: [:experience_id, :time], unless: -> {deal_id.zero? }
Why is it giving a "has already been taken" error when first it does first_or_initialize?
Thanks
It means you already have Booking with same deal_id in scope of experience_id, time
This could happen if you already had records in the database at the time when you added this uniqueness validation to model
But this is not necessary at all. Your first_or_initialize with just deal_id doesn't prevent of combination deal_id, experience_id and time
Besides, it doesn't protect you from race condition
You can check such records count with debug before updating:
Booking.where(
deal_id: booking.deal_id,
experience_id: booking.experience_id,
time: time,
).count
try to run without update method without ! to see where validation has failed
result = booking.update(
guests: guests,
names: names,
time: time)
result.errors # <= see related errors here
# This implies the first record
booking = Booking.find_by(deal_id: params["id"])
if booking.nil?
booking = Booking.new(
#... your attributes
)
else
# Update the attributes you want
booking.anything = anything
...
end
# This will create / update the record in the database
booking.save!
Related
I was trying to implement round robin assignment of leads to the members-users.
But when ever my function is called user cycle starts again, and all the leads are assigned to the first user. How can I assign next leads to the user that was next to the previously assigned user.
eg;
if lead1 is assigned to U2 and claimed, then next lead2 assignment should continue from U3.
Code that I used:
Rough:
def round_robin(selected_lead)
lead = selected_lead
users = %i(U1 U2 U3).cycle
while lead.status !=“Claimed” do
assigned_user=users.next
# assigned_user <= Assign lead to
# Check to wait 10 min for claimed response...
end
puts “Lead #{lead} has been assigned to #{assigned_user}
end
I was doing this in ruby language.
any suggestions would be appreciated.
You need to persist the concept of the next-user-to-be assigned a lead. I suggest doing this in a boolean column in the users table next. Only one user will ever have this attribute set to true, all others have it set to nil or false.
So in the User model, User.next is the next user that will receive a lead assignment
# in user.rb
has_many :leads
def self.next
find_by(next: true) || first
end
and User.next_next is the User after User.next where the order is simply the value of the id. (Can't just add 1 to the id, b/c there may be missing ids, due to deleted users)
# in user.rb
def self.next_next
# get the list of all User ids, and find the index of the one that has next=true
next_index = ids.index(next.id)
# find the index in the list of ids of the User after next, modulo User.count.
# So when you get to the last user, "round-robin" back to the first
next_next_index = next_index.succ % count
# return the User after next
find(ids[next_next_index])
end
When a lead is assigned, it is assigned to User.next, the value of that user's next attribute is set to false, and the value of the user after User.next has the value of the next attribute set to true
# in lead.rb
belongs_to :user
def assign_to_next_user
User.assign_lead(self)
end
# in user.rb
def self.assign_lead(lead)
next.leads << self
next.update(next: false)
next_next.update(next: true)
end
I am searching or initializing and object and then updating it:
booking = Booking.where(deal_id: params["id"]).first_or_initialize
if booking.update!(
names: "some names",
deal_id: params["id"]
)
some other stuff
is failing with the message
Validation failed: deal_id has already been taken
If I am calling first or initialize and then update, how is this error possible? What needs to be fixed? Is it because I am adding deal_id again on the update line?
Thanks
I have a User model which has_one :address, as: :addressable. Here address is a polymorphic association.
When saving the user, if it errors out. Because of failing validations, Errors are not grouped for address
The output of user.errors.to_json is
"{\"address.city\":[\"can't be blank\"],\"address.state\":[\"can't be blank\"],\"address.country\":[\"can't be blank\"],\"address.zipcode\":[\"can't be blank\",\"is invalid\"],\"address.business_phone\":[\"is invalid\"],\"name\":[\"can't be blank\"]}"
I would like the output to be
"address: {city: ["can't be blank], state: ["can't be blank]..}"
Is it possible to group the error messages or exclude the error messages of the association?
Because user.address.errors seem to give what I need, but I need to get the user model errors without the errors of address model.
A working solution
# Note: This modifies the given Hash
def extract_address_errors_from_user_errors!(user_errors_hash)
address_error_prefix = 'address.'
empty_str = ''
address_error_keys = []
address = Address.new
user_errors_hash.each do |k, v|
unless k.start_with?(address_error_prefix)
next
end
# Collect the address error keys to later remove them from user errors hash
address_error_keys << k
address_attr_name = k.gsub(address_error_prefix, empty_str)
address_attr_name_sym = address_attr_name.to_sym
address.errors.add(address_attr_name_sym, v)
address.errors[address_attr_name_sym].flatten!
end
# Remove the address errors from user errors
user_errors_hash.except!(*address_error_keys)
address.errors.to_hash
end
Usage
user_errors_hash = user.errors.to_hash
address_errors_hash = extract_address_errors_from_user_errors!(user_errors_hash)
user_errors_hash.merge!(address: address_errors_hash)
# This should give you the desired output
user_errors_hash.to_json
i have a model which defines size ranges like 100m² to 200m². I wrote a validator:
class SizeRange < ActiveRecord::Base
validate :non_overlapping
def non_overlapping
lower_in_range = SizeRange.where(lower_bound: lower_bound..upper_bound)
upper_in_range = SizeRange.where(upper_bound: lower_bound..upper_bound)
if lower_in_range.present?
errors.add(:lower_bound, 'blablabla')
end
if upper_in_range.present?
errors.add(:upper_bound, 'blablabla')
end
end
end
My guess was, that when I try to save a new model which lower or upper bound appears to be in the range of an other SizeRange instance the validator would mark the model as invalid and the save action would be aborted.
What really happened is that my model got saved and assigned an id, but when I call model.valid? it returns false (So my validator seems to do what it should).
Is there anything I could have done wrong, or did I not understand how the validators work? Can I force the validator to abort the save action?
Another question:
Is there any way to enforce a constraint like that through database constraints? I think I would prefer a solution on database side.
Thanks for your help!
model.save
would be accepted silently and return false. It will not throw any Exception.
You should call:
model.save!
to fail with validations
You should return false after each errors.add to cancel the record saving
Turns out the problem was my not well-formed validator function.
The case I checked an which led to confusion:
SizeRange from 200 to 400 already in the database
Creating a new one between 200 and 400 like SizeRange.new({:lower_bound=>250, :upper_bound=>350})
So I expected that to be invalid (model.valid? -> false) but it was, of course, valid. So model.save did no rollback and AFTER that I tested on model.valid? which would NOW return false, as the newly saved instance would violate the constraint, because it was tested against itself.
So there were two problems:
model.valid? would also test against the same instance if it already had an id
the validator would not validate what I thought it would
So I ended up rewriting my validator function:
def non_overlapping
sr = id.present? ? SizeRange.where.not(id: id) : SizeRange
ranges = sr.all
lower_overlappings = ranges.map { |r| lower_bound.between?(r.lower_bound, r.upper_bound)}
upper_overlappings = ranges.map { |r| upper_bound.between?(r.lower_bound, r.upper_bound)}
if lower_overlappings.any?
errors.add(:lower_bound, 'lower bla bla')
end
if upper_overlappings.any?
errors.add(:lower_bound, 'upper bla bla')
end
end
The first line handles the first problem and the rest handles the intended validation.
Thanks for your help and sorry for the confusion.
You should be using begin and rescue:
class SizeRange < ActiveRecord::Base
validate :non_overlapping
private
def non_overlapping
lower_in_range = SizeRange.where(lower_bound: lower_bound..upper_bound)
upper_in_range = SizeRange.where(upper_bound: lower_bound..upper_bound)
begin
raise("Lower Bound Met!") if lower_in_range.present?
rescue => ex
errors.add(:lower_bound, "#{ex.message} (#{ex.class})")
end
begin
raise("Lower Bound Met!") if upper_in_range.present?
rescue => ex
errors.add(:upper_bound, "#{ex.message} (#{ex.class})")
end
end
end
I have a method which creates a key value pair of delivery costs, the key being the type, and the value being the cost.
def calculate_scoped_job_delivery_costs
delivery_hash = {}
['install', 'fuel', 'breakdown'].each do |scope|
delivery_hash[scope.humanize] = job_delivery_costs.send(scope).inject(0) { |total, item| total + (item.cost_per_unit * item.hour_count) * item.quantity }
end
delivery_hash.delete_if {|key, value| value <= 0 }
end
the key is a scope in the job delivery costs model, which retrieves all associated costs with that scope and adds them up. It works, but I want to test its behaviour, albeit retrospectively.
So its core expected behaviour is:
it should output a hash
it should calculate each scope value
it should remove blank values from the hash
So I have written this test (factories posted below)
let(:jdc1){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc2){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc3){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
describe "calculate_scoped_job_delivery_costs" do
before do
allow(jdc1).to receive(:timing).and_return('fuel')
jdc2.update_attributes(quantity: 4)
jdc2.delivery_cost.update_attributes(timing: 'breakdown')
allow(job).to receive(:job_delivery_costs).and_return(JobDeliveryCost.where(id: [jdc1,jdc2,jdc3].map{|jdc| jdc.id}))
end
it "should retrieve a hash with jdc scopes" do
expect(job.calculate_scoped_job_delivery_costs.is_a?(Hash)).to be_truthy
end
it "should calculate each hash value" do
expect(job.calculate_scoped_job_delivery_costs).to eq "Fuel"=>15.0
end
it "should remove blank values from hash" do
expect(job.calculate_scoped_job_delivery_costs).to_not include "Breakdown"=>0
end
end
So in the last test, it passes, why? I have purposefully tried to make it break by updating the attributes in the before block on jdc2 so that breakdown is another scoped value.
Secondly, by changing the state of jdc2 and its values, this should break test 2 as fuel is no longer calculated against the same values.
Here are my factories...
FactoryGirl.define do
factory :job_delivery_cost do
job
delivery_cost
cost_per_unit 1.5
quantity 3
hour_count 1.0
end
end
FactoryGirl.define do
factory :delivery_cost do
title
timing "Fuel"
cost_per_unit 1.5
end
end
FactoryGirl.define do
factory :job do
job_type
initial_contact_id_placeholder {FactoryGirl.create(:contact).id}
title "random Title"
start "2013-10-04 11:21:24"
finish "2013-10-05 11:21:24"
delivery "2013-10-04 11:21:24"
collection "2013-10-05 11:21:24"
delivery_required false
collection_required false
client { Client.first || FactoryGirl.create(:client) }
workflow_state "offer"
admin
end
end
job has_many :job_delivery_costs.
job_delivery_cost belongs_to :delivery_cost
has_many :job_delivery_costs
has_many :jobs, through: :job_delivery_costs
I am really struggling with the logic of these tests, I am sure there are more holes than what I have laid out above. I welcome criticism in that regard.
thanks
A couple of suggestions:
Remember that let is lazy-evaluated; factories within the block are not created until the symbol defined by let is encountered in the code. This can have unexpected consequences for things like scopes, where you might think that the database already includes your factory-generated rows. You can get around this by using let!, which is evaluated immediately, or by restructuring the spec to ensure things get created in the right order.
I prefer not to do partial stubbing where it can be avoided. For scopes, you are probably better off using factories and just letting the scopes retrieve the rows instead of stubbing the relations. In your case this means getting rid of the code in the before block and setting up each example with factories, so that the scopes are retrieving the expected values.