ActiveRecord: abort datetime parsing if value is invalid - ruby-on-rails

I have a datetime field in my ActiveRecord model (in Rails) and I need to protect my model from crashes when date fieid is out of ranges.
I found this question and and tried to make similarly:
class Something < ActiveRecord::Base
validate :important_date_is_valid_datetime, on: :create
validates :important_date, presence: true
private
def important_date_is_valid_datetime
if (DateTime.parse(important_date.to_s()) rescue ArgumentError) == ArgumentError then
errors.add(:important_date, 'must be a valid datetime')
end
end
end
This code can 'validate' datetime field but it don't abort field parsing if valie is out of range:
irb(main):007:0> Something.new(important_date: "2015-20-40").valid?
ArgumentError: argument out of range
from /home/shau-kote/.gem/ruby/2.1.0/gems/activesupport-4.2.0/lib/active_support/values/time_zone.rb:350:in `initialize'
from /home/shau-kote/.gem/ruby/2.1.0/gems/activesupport-4.2.0/lib/active_support/values/time_zone.rb:350:in `new'
from /home/shau-kote/.gem/ruby/2.1.0/gems/activesupport-4.2.0/lib/active_support/values/time_zone.rb:350:in `parse'
...
Can I prevent similar crashes?

This is an issue with Rails 4 where the exception gets thrown before your validators are even called. There is a pull request that brings back the behavior of Rails 3 where a invalid date is just nil. This pull request seems to be merged, so maybe updating your Rails version can already fix the problem.
For the meantime I can think of two workarounds: First, you could use client side validations that of course can be tampered with if wanted but would protect users using your app in "good faith" and second, instead of using a validation you could be checking for a valid date (or the ArgumentError respectively) in the controller before the parameter is used to set the datetime attribute. Both options are not optimal but should prevent more crashes.

Related

Record being created multiple times in spite of validation (timing issue)

A webhook with lots of processing creates a record that has to go through this validation:
validates_uniqueness_of :pipedrive_deal_id, :allow_blank => true, scope: [:experience_id, :time], unless: -> { pipedrive_deal_id.zero? }
The processing of this webhook generates additional calls of the same webhook. This results in the same record being created 4 times, not respecting the validation shown above.
I "fixed" the issue by putting "sleep 5" on subsequent calls of the webhook, which apparently gives the initial process enough time to "close"? the validation and rejects any subsequent attempts to create it.
Is there a less hacky way to fix this issue?
Here's the code of the method (I have removed some things to make it clearer):
def self.create_or_update_from_pipedrive(params, won_or_update,changed_values=nil)
#this method calls the same webhook
hosting_link = PipedriveService.hosting_platform_setup(params[:id])
if won_or_update == ('won')
Payment.add_deal(params)
else
#this is the offending sleep 5
sleep 5
end
#....internal processing
if booking.update( booking_params)
if booking.tour.present?
if booking.tour.time != booking.time
#this calls the webhook again
booking.add_to_tour
end
end
end
if params[Rails.configuration.pipedrive_hash[:trip_details]].include? '40'
if booking.update(booking_params)
if booking.tour.present?
if booking.tour.time != booking.time
#this calls the webhook again
booking.add_to_tour
end
end
end
#this calls the webhook again
PipedriveService.set_post_session_activities(params, booking_time) if won_or_update == 'won'
end
Thanks
You need a unique index in the database. It is even mentioned in the official rails guides.
https://guides.rubyonrails.org/active_record_validations.html#uniqueness
This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index on that column in your database.
In case anyone stumbles upon my issue, the solution is (as suggested by the other answers) a unique index in the DB. As for the "unless attribute is zero" part, here is the whole migration command to add this as well:
add_index :bookings, [:pipedrive_deal_id, :time,:experience_id], unique: true, where: 'pipedrive_deal_id > 0'

Validation Confirmation - Change Confirmation From String to Integer

I'm using Rail's ActiveRecord Validation Confirmation to validate an id. I have two form fields, cbiz_id and cbiz_id_confirmation, so that the fields receive exactly the same content.
cbiz_id is an integer and ActiveRecord makes cbiz_id_confirmation a string.
Is there a way to validate cbiz_id_confirmation as an integer?
validates :cbiz_id, confirmation: true, presence: { if: :has_any_qapps_role?, notice: I18n.t('messages.updated') }
I've attempted a number of things such as:
before_validation :change_cbiz_id_confirmation_to_integer
def change_cbiz_id_confirmation_to_integer
:cbiz_id_confirmation.to_i
end
I'm new to Ruby and Rails, so foundational explanations are appreciated!
Rails converts cbiz_id because it looks at the DB can sees that the column is an integer. Since there's no column for cbiz_id_confirmation, Rails doesn't have any type data, and the default type for any form submission (since form encoding and query params have no types) is a string.
Your change_cbiz_id_confirmation_to_string should work, but you're calling to_i on a symbol. You want just cbiz_id_confirmation.to_i (no leading :). Also, perhaps that method should end in to_integer.
If you want to ensure cbiz_id_confirmation is an integer even outside of validations, you can write a setter method that does the conversion for you.
attr_reader :cbiz_id_confirmation
def cbiz_id_confirmation=(value)
#cbiz_id_confirmation = Integer(value)
rescue TypeError
#cbiz_id_confirmation = nil
end
Integer(value) will raise an exception if the value cannot be converted (eg. it is nil or the word "ham"), so the rescue block makes this act like Rails and treat invalid data as nil. You can also use value.to_i, which that will coerce nil and "ham" to 0, or just let the exception happen and handle the problem elsewhere.
I solved the issue with the following code:
attr_accessor :current_user_qapps_role, :cbiz_id_confirmation
validates :cbiz_id, confirmation: true, presence:true, if: :has_any_qapps_role?
def cbiz_id_confirmation=(val)
#cbiz_id_confirmation = val.to_i
end
You can easily create a custom validation rule like this, but there's probably some more elegant solution to your problem if I'd understand the context:
validate :cbiz_matches
def cbiz_matches
errors.add(:cbiz_id, "ID's don't match") unless self.cbiz_id.to_i == self.cbiz_id_confirmaton.to_i
end

How do I recognize invalid dates in rails?

When modifying Active Record object's attributes (like Task.update date: '2015-01-01'), I'd like to know when the value is invalid ('2015-01-32' or 'whatever'). Right now, activerecord just writes nil in case of those, not very friendly I must say. Previous value is lost. I'd like to leave it there, if user inputted nothing like date.
Is there any more sensible way to figure it out than the following?
if ActiveRecord::Attribute.from_user('date', '2015-01-32', ActiveRecord::Type::Date.new).value
puts 'valid date'
end
P.S. Just in case someone wants to know how type casting happens.
If there's no way to tell activerecord to not discard values, and no public interface for checking if a date is valid, than the best option must be to use validation:
class Task < ActiveRecord::Base
validates_date :date
end
you can try to parse it and when it raise error then its nit valid:
valid = Date.parse('2015-01-30').present? rescue false # true
valid = Date.parse('2015-01-32').present? rescue false # false

How to validate (and handle) if value passed to .to_date is valid or returns error?

If user inserts a value that is not accepted to the to_date method, e.g. "some gibberish", it returns:
"ArgumentError: invalid date"
How could I construct a custom validation that returns an validation message to user and doesn't halts the application?
I was thinking that reusing the code from another answer would be a good idea, but I don't know how to apply it to to_date.
validate do
self.errors[:start] << "must be a valid date" unless DateTime.parse(self.start) rescue false
end
It could be achieved with rescue, and then addes to errors
class Post < ActiveRecord::Base
validate do |post|
begin
Date.parse(post.date)
rescue ArgumentError
errors.add(:date, "must be a valid date")
end
end
## Another way
validate do |post|
# make sure that `date` field in your database is type of `date`
errors.add(:date, "must be a valid date") unless post.date.try(to_date)
end
end
Rather than writing your own validation (and having to write tests for it!) I suggest the validates_timeliness gem
Using it is as simple as
validates_date :attribute_name

calling custom validation methods in Rails

I just upgraded my rails to 2.3.4 and I noticed this with validations:
Lets say I have a simple model Company which has a name. nothing to it.
I want to run my own validation:
class Company < ActiveRecord::Base
validate :something
def something
false
end
end
saving the model actually works in this case.
The same thing happens if i override validate() and return false.
I noticed this in a more complex model where my validation was returning false, but the object was still saving...I tried it out in an essentially empty model and the same thing applied. Is there a new practice I am missing? This doesn't seem to be the case in some of my older rails code.
Your validations are executed when you use the validate method. However rails doesn't relies on the returned value.
It relies on if there are validations errors or not. So you should add errors when your model doesn't validates.
def something
errors.add(:field, 'error message')
end
Or, if the error is not related to a field :
def something
errors.add(:base, 'error message')
end
Then your model won't be saved because there are errors.
You're getting confused between validations and callbacks.
Validations are supposed to fail if there are any errors on the object, doesn't matter what the validation returns. Callbacks fail if they return false, regardless if they add any errors to object.
Rails uses calls valid? from save calls which does not check the result of any validations.
Edit: Rails treats validate :method as a callback, but valid? still doesn't check for their results, only for errors they added to the object.
I don't think this behaviour changed at all but I could be wrong. I don't think I've ever written a validation to return false before.
Just FYI errors.add_to_base('error message') has been deprecated in rails 3 and got replaced by
errors[:base] << "Error message"
Or
errors.add(:base, "Error message")

Resources