Rails: validation fails with before_create method on datetime attribute - ruby-on-rails

ApiKey.create!
Throws a validation error: expires_at can't be blank.
class ApiKey < ActiveRecord::Base
before_create :set_expires_at
validates :expires_at, presence: true
private
def set_expires_at
self.expires_at = Time.now.utc + 10.days
end
end
with attribute
t.datetime :expires_at
However, if the validation is removed the before_create method works on create.
Why is this? - This pattern works for other attributes, e.g. access_tokens (string), etc.

I would say because the before_create runs after the validation, maybe you want to replace before_create with before_validation
Note: If you leave the call back like that, it would set the expiry date whenever you run valid? or save or any active record method that fires the validation, You might want to limit this validation to the creation process only
before_validation :set_expires_at, on: :create
This will limit the function call only when the creation is run first time.

Related

How to validate data that was altered in before_save

I have several validations that validate a Quote object. Upon validation, I have a before_save callback that calls an API and grabs more data, makes a few math computations and then saves the newly computed data in the database.
I don't want to trust the API response entirely so I need to validate the data I compute.
Please note that the API call in the before_save callback is dependent on the prior validations.
For example:
validates :subtotal, numericality: { greater_than_or_equal_to: 0 }
before_save :call_api_and_compute_tax
before_save :compute_grand_total
#I want to validate the tax and grand total numbers here to make sure something strange wasn't returned from the API.
I need to be able to throw a validation error if possible with something like:
errors.add :base, "Tax didn't calculate correctly."
How can I validate values that were computed in my before_save callbacks?
you can use after_validation
after_validation :call_api_and_compute_tax, :compute_grand_total
def call_api_and_compute_tax
return false if self.errors.any?
if ... # not true
errors.add :base, "Tax didn't calculate correctly."
end
end
....
Have you tried adding custom validation methods before save? I think this is a good approach to verify validation errors before calling save method
class Quote < ActiveRecord::Base
validate :api_and_compute_tax
private
def api_and_compute_tax
# ...API call and result parse
if api_with_wrong_response?
errors.add :base, "Tax didn't calculate correctly."
end
end
end
Then you should call it like
quote.save if quote.valid? # this will execute your custom validation

Order/precedence of rails validation checks

I'm trying to get my head around the order/precedence with which Rails processes validation checks. Let me give an example.
In my model class I have these validation checks and custom validation methods:
class SupportSession < ApplicationRecord
# Check both dates are actually provided when user submits a form
validates :start_time, presence: true
validates :end_time, presence: true
# Check datetime format with validates_timeliness gem: https://github.com/adzap/validates_timeliness
validates_datetime :start_time
validates_datetime :end_time
# Custom method to ensure duration is within limits
def duration_restrictions
# Next line returns nil if uncommented so clearly no start and end dates data which should have been picked up by the first validation checks
# raise duration_mins.inspect # Returns nil
# Use same gem as above to calculate duration of a SupportSession
duration_mins = TimeDifference.between(start_time, end_time).in_minutes
if duration_mins == 0
errors[:base] << 'A session must last longer than 1 minute'
end
if duration_mins > 180
errors[:base] << 'A session must be shorter than 180 minutes (3 hours)'
end
end
The problem is that Rails doesn't seem to be processing the 'validates presence' or 'validates_datetime' checks first to make sure that the data is there in the first place for me to work with. I just get this error on the line where I calculate duration_mins (because there is no start_time and end_time provided:
undefined method `to_time' for nil:NilClass
Is there a reason for this or have I just run into a bug? Surely the validation checks should make sure that values for start_time and end_time are present, or do I have to manually check the values in all of my custom methods? That's not very DRY.
Thanks for taking a look.
Rails will run all validations in the order specified even if any validation gets failed. Probably you need to validate datetime only if the values are present.
You can do this in two ways,
Check for the presence of the value before validating,
validates_datetime :start_time, if: -> { start_time.present? }
validates_datetime :end_time, if: -> { end_time.present? }
Allows a nil or empty string value to be valid,
validates_datetime :start_time, allow_blank: true
validates_datetime :end_time, allow_blank: true
Simplest way, add this line right after def duration_restrictions
return if ([ start_time, end_time ].find(&:blank?))
Rails always validate custom method first.

Rails 4 attr_readonly on update

I want to make an attribute read only for each record after the first update, is there a way to do this in Rails 4 using attr_readonly? Or another method?
It has to do with Transaction security...
attr_readonly is a class method, so it would "freeze" the attribute on all instances of the model, whether they've been updated or not.
The sanest way to do what you want, I think, would be to add a some_attribute_is_frozen boolean attribute to your model, and then set it to true in a before_update callback. Then you can have a validation that will only run if some_attribute_is_frozen? is true, and which will fail if the "frozen" attribute has changed.
Something like this (for the sake of an example I've arbitrarily chosen "Customer" as the name of the model and address as the name of the attribute you want to "freeze"):
# $ rails generate migration AddAddressIsFrozenToCustomers
# => db/migrate/2014XXXX_add_address_is_frozen_to_customers.rb
class AddAddressIsFrozenToCustomers < ActiveRecord::Migration
def change
add_column :customers, :address_is_frozen, :boolean,
null: false, default: false
end
end
# app/models/customer.rb
class Customer < ActiveRecord::Base
before_update :mark_address_as_frozen
validate :frozen_address_cannot_be_changed, if: :address_is_frozen?
# ...snip...
private
def mark_address_as_frozen
self.address_is_frozen = true
end
def frozen_address_cannot_be_changed
return unless address_changed?
errors.add :address, "is frozen and cannot be changed"
end
end
Since the before_update callback runs after validation, the very first time the record is updated, address_is_frozen? will return false and the validation will be skipped. On the next update, though, address_is_frozen? will return true and so the validation will run, and if address has changed the validation will fail with a useful error message.
I hope that's helpful!

Is it possible to create a callback for a changed mongoid embedded document field in Ruby on Rails?

Is there a way to run a callback only if an embedded document field was changed?
Currently, the following runs the callback on a normal field only if it was changed:
class user
field :email, type: String
embeds_many :connections, cascade_callbacks: true
before_save :run_callback, :if => :email_changed?
before_save :run_connection_callback, :if => :connections_changed? # DOES NOT WORK
end
For anybody seeing this answer in 2015
In Mongoid 4.x model.changed? and model.changes exist and behave like their ActiveRecord counterparts.
Mongoid won't define the method connections_changed? for you, but you can define it yourself by using a virtual field in User to keep track of when an embedded connection gets changed. That is:
class User
# define reader/writer methods for #connections_changed
attr_accessor :connections_changed
def connections_changed?
self.connections_changed
end
# the connections are no longer considered changed after the persistence action
after_save { self.connections_changed = false }
before_save :run_connection_callback, :if => :connections_changed?
end
class Connection
embedded_in :user
before_save :tell_user_about_change, :if => :changed?
def tell_user_about_change
user.connections_changed = true
end
end
One shortcoming of this method is that user.connections_changed only gets set when the document is saved. The callbacks are cascaded in such a way that the Connection before_save callback gets called first and then the User before save callback, which allows the above code to work for this use case. But if you need to know whether any connections have changed before calling save, you'll need to find another method.

how to validate a record in table before saving in ruby on rails

I am new to Ruby on Rails
I have a scenario in which I have a form which has some fields. One of the field values I need to validate against a table which has the data .
I want to restrict the user from saving any data unless the field is validated with the table records.
Initially I added the code in controller to validate that but I have other fields which I need to validate as empty so it did not work .
Also I want the the validation error to be part of other errors.
I tried the below code in the model file
before_create :validate_company_id
def validate_company_id
cp = Company.find_by_company_id(self.company)
if #cp != nil
return
else
self.status ||= "Invalid"
end
end
But its not validating , could you help me how I can validate it .
regards
Surjan
The guys answered correctly, but provided the other way for solution. You could ask yourself: "Why doesn't my code get executed?"
First of all, you have errors in your code - #cp is undefined. Also, I don't know what are you trying to achieve with self.status ||= "Invalid".
You also don't have to use self when you're calling an attribute, but you do have to call it when you're assignig a new attribute value. So self.company is unnecessary, you can just use company.
I've also noticed you have the company_id attribute in your companies table. That's not neccessary, common convention is using just an id instead. If you don't want to alter your table you can set the id field on your model like so:
class Company < ActiveRecord::Base
set_primary_key :company_id
# ... the rest of your model code ...
end
After that you can use Company.find instead of Company.find_by_company_id.
Okay, let's say you have the following code after the fixes:
before_create :validate_company_id
def validate_company_id
cp = Company.find(company)
if cp != nil
return
else
self.status ||= "Invalid"
end
end
First of all I would like to use ternary operator here
before_create :validate_company_id
def validate_company_id
Company.find(company) ? return : self.status ||= "Invalid"
end
Isn't this cleaner? It does the exact same thing.
Now about that self.status of yours. If you would like to invalidate the object in ActiveModel you have to set some values in errors hash. You're in misconception if you think that a model with the status attribute of "Invalid" is invalid. It's still perfectly valid model in Rails.
So how do you invalidate?
You put some values into errors hash. You can also specify a message and the attribute you're validation error refers to.
So let's do it on your model
before_create :validate_company_id
def validate_company_id
Company.find(company) ? return : errors.add(:company,"Invalid Company ID")
end
Now if you try to save your model with invalid company_id it will still pass and get saved to the DB. Why is that?
It's because of the ActiveModels lifecycle. Your method gets called too late.
Here are all the callback methods you can use
Creating
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
Updating
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
Destroying
before_destroy
around_destroy
after_destroy
Notice how your method gets called long after the validation cycle. So you should not use before_create, but after_validation or before_validation callbacks instead.
And here we are with the working validation method of your model.
after_validation :validate_company_id
def validate_company_id
Company.find(company) ? return : errors.add(:company,"Invalid Company ID")
end
instead of using before_create. You can tell the model to use a custom method for validation as follows
validate :validate_company_id
def validate_company_id
cp = Company.find_by_company_id(self.company)
if cp.nil?
errors.add(:company, 'Invalid Company ID')
end
end
Inside you model, you can add custom validations as below:
validate :validate_company_id
def validate_company_id
Your validations
Add error messages can be added as below
errors.add(:company, "is invalid")
end

Resources