Update fails first time, succeeds second time - ruby-on-rails

We've got this object, #current_employer, that's acting a bit weird. Update fails the first time, succeeds the second.
(byebug) #current_employer.update(settings_params)
false
(byebug) #current_employer.update(settings_params)
true
Here's where we initialise it:
#current_employer = Employer.find(decoded_auth_token[:employer_id])
It's just a standard "find".
Current workaround:
if #current_employer.update(settings_params) || #current_employer.update(settings_params)
...
Anyone seen this before?
Update
Tracked it down to this line in a "before_save" call
# self.is_test = false if is_test.nil?
Seems like is_test is a reserved keyword?

Solved
The full callback, with the fix commented inline:
def set_default_values
self.has_accepted_terms = false if has_accepted_terms.nil?
self.live = true if live.nil?
self.account_name.downcase!
self.display_name ||= account_name
self.display_name ||= ""
self.contact_first_name ||= ""
self.contact_last_name ||= ""
self.phone_number ||= ""
self.is_test_account = false if is_test_account.nil?
true # FIX. The proceeding line was returning 'false', which was giving us a 'false' return value for the before_save callback, preventing the save.
end

Model
If it's failing in one instance and succeeding almost immediately afterwards, the typical issue is that you're passing incorrect / conflicting attributes to the model.
I would speculate that the settings_params you're sending have a value which is preventing the save from occurring. You alluded to this with your update:
# self.is_test = false if is_test.nil?
The way to fix this is to cut out any of the potentially erroneous attributes from your params hash:
def settings_params
params.require(:employer).permit(:totally, :safe, :attributes)
end
Your model should update consistently - regardless of what conditions are present. If it's failing, it means there'll be another problem within the model save flow.
--
Without seeing extra information, I'm unable to see what they may be
Update
A better way to set default values is as follows:
How can I set default values in ActiveRecord?
You may wish to use the attribute-defaults gem:
class Foo < ActiveRecord::Base
attr_default :age, 18
attr_default :last_seen do
Time.now
end
end

Related

Getting "original" object during a before_add callback in ActiveRecord (Rails 7)

I'm in the process of updating a project to use Ruby 3 and Rails 7. I'm running into a problem with some code that was working before, but isn't now. Here's (I think) the relevant parts of the code.
class Dataset < ActiveRecord::Base
has_and_belongs_to_many :tags, :autosave => true,
:before_add => ->(owner, change){ owner.send(:on_flag_changes, :before_add, change) }
before_save :summarize_changes
def on_flag_changes(method, tag)
before = tags.map(&:id)
after = before + [tag.id]
record_change('tags', before, after)
end
def record_change(field, before_val, after_val)
reset_changes
before_val = #change_hash[field][0] if #change_hash[field]
if before_val.class_method_defined? :sort
before_val = before_val.sort unless before_val.blank?
after_val = after_val.sort unless after_val.blank?
end
#change_hash[field] = [before_val, after_val]
end
reset_changes
if #change_hash.nil?
#change_notes = {}
#change_hash = {
tags: [tags.map(&:id), :undefined]
}
end
end
def has_changes_to_save?
super || !change_hash.reject { |_, v| v[1] == :undefined }.blank?
end
def changes_to_save
super.merge(change_hash.reject { |_, v| v[0] == v[1] || v[1] == :undefined })
end
def summarize_changes
critical_fields = %w[ tags ]
#change_notes = changes_to_save.keep_if { |key, _value| critical_fields.include? key } if has_changes_to_save?
self.critical_change = true unless #change_notes.blank?
end
There are more fields for this class, and some attr_accessors but the reason I'm doing it this way is because the tags list can change, which may not necessarily trigger a change in the default "changes_to_save" list. This will allow us to track if the tags have changed, and set the "critical_change" flag (also part of Dataset) if they do.
In previous Rails instances, this worked fine. But since the upgrade, it's failing. What I'm finding is that the owner passed into the :before_add callback is NOT the same object as the one being passed into the before_save callback. This means that in the summarize_changes method, it's not seeing the changes to the #change_hash, so it's never setting the critical_change flag like it should.
I'm not sure what changed between Rails 6 and 7 to cause this, but I'm trying to find a way to get this to work properly; IE, if something says dataset.tags = [tag1, tag2], when tag1 was previously the only association, then dataset.save should result in the critical_change flag being set.
I hope that makes sense. I'm hoping this is something that is an easy fix, but so far my looking through the Rails 7 documentations has not given me the information I need. (it may go without saying that #change_notes and #change_hash are NOT persisted in the database; they are there just to track changes prior to saving to know if the critical_change flag should be set.
Thanks!
Turns out in my case there was some weird caching going on; I'd forgotten to mention an "after_initialize" callback that was calling the reset method, but for some reason at the time it makes this call, it wasn't the same object as actually got loaded, but some association caching was going on with tags (it was loading the tags association with the "initialized" record, and it was being cached with the "final" record, so it was confusing some of the code).
Removing the tags bit from the reset method, and having it initialize the tag state the first time it tries to modify tags solved the problem. Not particularly fond of the solution, but it works, and that's what I needed for now.

Rails ActiveRecord custom validator returns false but model.save is accepted without rollback

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

rubocop app controller function validate param integer use of nil? predicate

I tried rewriting this function numerous ways to get around this error, however, I want to defer to other experts before I disable the cop around it.
def numeric?(obj)
obj.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true
end
This is used like so:
def index
if params[:job_id] && numeric?(params[:job_id])
This issue was solved via: Checking if a variable is an integer
Update trying:
def numeric?(string)
!!Kernel.Float(string)
rescue TypeError, ArgumentError
false
end
Reference How do I determine if a string is numeric?
New error:
def numeric?(arg)
!/\A[+-]?\d+\z/.match(arg.to_s).nil?
end
Passes all Rubocop tests from a default configuration. Complete gist with tests at https://gist.github.com/aarontc/d549ee4a82d21d263c9b
The following code snippet does the trick:
def numeric?(arg)
return false if arg.is_a?(Float)
return !Integer(arg).nil? rescue false
end
Returns false for the following: 'a', 12.34, and '12.34'.
Returns true for the following: '1', 1.
You can write the method
def numeric?(obj)
obj.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/).nil?
end
You really don't need to do nil comparisons and then based on the decision returning true/false. #nil? method does it for you.

Rails - 'can't dump hash with default proc' during custom validation

I have 2 models. User and Want. A User has_many: Wants.
The Want model has a single property besides user_id, that's name.
I have written a custom validation in the Want model so that a user cannot submit to create 2 wants with the same name:
validate :existing_want
private
def existing_want
return unless errors.blank?
errors.add(:existing_want, "you already want that") if user.already_wants? name
end
The already_wants? method is in the User model:
def already_wants? want_name
does_want_already = false
self.wants.each { |w| does_want_already = true if w.name == want_name }
does_want_already
end
The validation specs pass in my model tests, but my feature tests fail when i try and submit a duplicate to the create action in the WantsController:
def create
#want = current_user.wants.build(params[:want])
if #want.save
flash[:success] = "success!"
redirect_to user_account_path current_user.username
else
flash[:validation] = #want.errors
redirect_to user_account_path current_user.username
end
end
The error I get: can't dump hash with default proc
No stack trace that leads to my code.
I have narrowed the issue down to this line:
self.wants.each { |w| does_want_already = true if w.name == want_name }
if I just return true regardless the error shows in my view as I would like.
I don't understand? What's wrong? and why is it so cryptic?
Thanks.
Without a stack trace (does it lead anywhere, or does it just not appear?) it is difficult to know what exactly is happening, but here's how you can reproduce this error in a clean environment:
# initialize a new hash using a block, so it has a default proc
h = Hash.new {|h,k| h[k] = k }
# attempt to serialize it:
Marshal.dump(h)
#=> TypeError: can't dump hash with default proc
Ruby can't serialize procs, so it wouldn't be able to properly reconstitute that serialized hash, hence the error.
If you're reasonably sure that line is the source of your trouble, try refactoring it to see if that solves the problem.
def already_wants? want_name
wants.any? {|want| want_name == want.name }
end
or
def already_wants? want_name
wants.where(name: want_name).count > 0
end

Rails - Exclude an attribute from being saved

I have a column named updated_at in postgres. I'm trying to have the db set the time by default. But Rails still executes the query updated_at=NULL. But postgres will only set the timestamp by default when updated_at is not in the query at all.
How do I have Rails exclude a column?
You can disable this behaviour by setting ActiveRecord::Base class variable
record_timestamps to false.
In config/environment.rb, Rails::Initializer.run block :
config.active_record.record_timestamps = false
(if this doesn't work, try instead ActiveRecord::Base.record_timestamps = false at the end of the file)
If you want to set only for a given model :
class Foo < ActiveRecord::Base
self.record_timestamps = false
end
Credit to Jean-François at http://www.ruby-forum.com/topic/72569
I've been running into a similar issue in Rails 2.2.2. As of this version there is an attr_readonly method in ActiveRecord but create doesn't respect it, only update. I don't know if this has been changed in the latest version. I overrode the create method to force is to respect this setting.
def create
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
quoted_attributes = attributes_with_quotes(true, false)
statement = if quoted_attributes.empty?
connection.empty_insert_statement(self.class.table_name)
else
"INSERT INTO #{self.class.quoted_table_name} " +
"(#{quoted_attributes.keys.join(', ')}) " +
"VALUES(#{quoted_attributes.values.join(', ')})"
end
self.id = connection.insert(statement, "#{self.class.name} Create",
self.class.primary_key, self.id, self.class.sequence_name)
#new_record = false
id
end
The change is just to pass false as the second parameter to attributes_with_quotes, and use quoted_attributes.keys for the column names when building the SQL. This has worked for me. The downside is that by overriding this you will lose before_create and after_create callbacks, and I haven't had time to dig into it enough to figure out why. If anyone cares to expand/improve on this solution or offer a better solution, I'm all ears.

Resources