I have a model
class Vehicule < ActiveRecord::Base
before_validation :set_principal, :if =>:new_record?
private
def set_principal
self.principal ||= !!principal
end
end
If I test it, when I do Vehicule.new.valid? it always return false. Why did I had to add ̀€true in the function to pass the test ?
private
def set_principal
self.principal ||= !!principal
true
end
For starters, failed validations populate an errors object in your record instance. You can see the errors by running model.errors.full_messages which is always useful for debugging.
Secondly, test for the presence of a value using the present? method rather than using the awkward !! approach which returns false when called on nil. It would make your code much more clear but it's also useful because it recognizes "empty" objects as well. In other words "".present? == false and [].present? == false and {}.present? == false nil.present? == false and so on. There's also the blank? method which is the logical opposite of present?.
Finally, be mindful of your return value when using the :before_validation callback. Unlike other callbacks it will prevent validation if the function returns false.
because not not nil => false.
In IRB:
foo = nil
!!foo => false
if you try Vehicule.new(principal: 'WORKS').valid?
def set_principal
self.principal ||= !!principal
end
It should pass.
Related
I have created a custom validation for a ruby model (using the luhn algorithm). Even when I explicitly just return false from the custom validation, the object still saves.
This is what I have in my CreditCard model:
before_save :check_card_number
private
def check_card_number
return false unless card_number_luhn
end
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
but even if I just return false:
before_save :check_card_number
private
def check_card_number
return false
end
#so this is never even called
def card_number_luhn
#luhn_algorithm_here_that_returns_true_or_false
end
the object still saves. This is true EVEN IF I use validate instead of before_save. What is going on?
From Rails 5, you need to explicitly call throw(:abort)
# ...
before_save :check_card_number
# ...
def card_number_luhn
valid = #luhn_algorithm_here_that_returns_true_or_false
throw(:abort) unless valid
end
another way:
ActiveSupport.halt_callback_chains_on_return_false = true
reference: https://www.bigbinary.com/blog/rails-5-does-not-halt-callback-chain-when-false-is-returned
This is the first time I am writing test cases on a rails project which is using RSpec and FactoryGirl
When I run the test case i get the following error
wrong number of arguments (given 0, expected 1)
I have gone through other posts at stack over flow and they are not much helpful in my case.
What I have tried
I am writing a test case on a Model which is called ImportFeed and it looks something like as following
class ImportFeed < ApplicationRecord
belongs_to :staffroom
belongs_to :user, optional: true # We don't have to have a user
validates_presence_of :url, :feed_type
validates :enabled, presence: true, allow_blank: true
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
This is what my test case looks like
require 'rails_helper'
describe JobImporters::JoraJobImporter, '.run' do
it 'should create an instance of ImportFeed' do
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
expect(feed).to be_a ImportFeed
end
end
This is the factory
FactoryGirl.define do
factory :import_feed do
trait :import1 do
enabled true
feed_type 'example'
staffroom_id 7526
url Faker::Internet::url
end
end
end
When I run this I get the error mentioned at the beginning of this question,
If I pass the data to the test case without FactoryGirl then my test case works and passes for example if I replace
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
with
feed = ImportFeed::new enabled: true, staffroom_id: 7526, feed_type: 'example', url: Faker::Internet::url
the test case passes.
I will really appreciate if someone can point to me what am I doing wrong here.
Because you're overriding initialize method, so you got unexpected exception.
Don't override initialize on ActiveRecord objects
ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. [link]
In order to solve your problem, you should set your attributes in callback instead
class ImportFeed < ApplicationRecord
# ...
after_initialize :set_my_attributes
private
def set_my_attributes
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
One more thing:
You're testing creating an instance of ImportFeed functionality, so you should either pass params to new or create methods to test it, but you pass an instance of ImportFeed to it (from FactoryGirl).
According to the docs, ActiveRecord#new accepts Hash only (the default argument is {} if you don't pass anything).
If you pass an object to it, you'll get ArgumentError exception along with "When assigning attributes, you must pass a hash as an argument" message
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
I think you are just using the initialize method to change the values according to the conditions:
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
You should not override it (my suggestion) as it may break many things. So instead you may change the values on a callback (before_validation, before_save, before_create, after_initialize whichever suits you) like this:
before_create :set_default_radius, if: proc { |feed| feed.default_radius.blank? }
def set_default_radius
self.default_radius = DEFAULT_RADIUS
end
And the best way to do this is having the default value in database itself. You can define that in migration:
def up
change_column :import_feeds, :default_radius, :integer, default: 0
end
def down
change_column :import_feeds, :default_radius, :integer, default: nil
end
So if the value is not defined it will always set to the default value mentioned in the migration file.
Also you may have a read of several question related to this which has some very good answers and explanation:
How to override "new" method for a rails model
Why is overriding ActiveRecord::Base.initialize wrong?
Overriding ApplicationRecord initialize, bad idea?
I have a model with a field where I am validating content in the field. I am using ActiveRecord validations to check for presence of that content. However, I want to be able to save, update, etc, without checking the validity. I do want to get the validity at one specific time, and retrieve the errors from it.
validates :my_content_in_field, presence: true, if: :should_validate
attr_accessor :should_validate
I want this to pass
valid?
and this to fail
valid?(should_validate: true)
And after the failed validation I want all updates and saves to work per usual. Is this possible? I essentially want to leverage ActiveRecords error messages, but not actually validate otherwise.
At the end of the day on Friday, I may just be missing something obvious.
I'm not sure you can call valid?(should_validate: true). valid? method may be called with one parameter called context (see docs). You should read a great post on validation contexts.
This should work:
class MyModel < ActiveRecord::Base
validates :something, on: :should_validate
...
end
m = MyModel.new
m.valid? # No validation happens
m.valid?(:should_validate) # Validates something
I think the most efficient way is defining a specific method in your code like this:
class Model < ActiveRecord::Base
validates :my_content_in_field, presence: true, if: :should_validate
attr_accessor :should_validate
alias :super_valid? :valid?
# Define a custom loose validation method that
# will ignore :my_content_in_field
#
# As for the following method, the `context` param
# is needed to replicate the default signature
# of `ActiveRecord::Validations::valid?`
def loosely_valid?(context = nil)
#should_validate = false
super_valid? context
end
# Tweak the base valid? method
def valid?(context = nil)
#should_validate = true
super_valid? context
end
end
You will always validate the :my_content_in_field parameter using the standard valid? method unless when you call loosely_valid? that will ignore that parameter forcing the #should_validate attribute in your model.
This is achieved using a simple alias call at the beginning of the method to override the standard valid? method from ActiveRecord.
This approach will always validate :my_content_in_field when creating/updating the model. If you don't need this you can change the code changing those methods like this:
def strictly_valid?(context = nil)
#should_validate = true
super_valid? context
end
def valid?(context = nil)
#should_validate = false
super_valid? context
end
The best I've been able to come up with is creating a custom method:
validates :my_content_in_field, presence: true, if: :should_validate
attr_reader :should_validate
def has_valid_data?
#should_validate = true
stored_valid = valid?
#should_validate = false
stored_valid
end
So now I get the desired behavior:
valid? #=> true
has_valid_data? #=> false
And I can collect my ActiveRecord error messages and be on my way. But I'd love to see a better way.
If you're just trying to leverage ActiveRecord error messages and you never need valid? to consider this field, a better approach would be to use the errors API directly:
def valid_data?
valid = valid?
errors.add(:my_field, 'invalid content') unless custom_field_is_valid? # your logic here
valid && errors.empty?
end
This will run the validation on all your other fields, adding their error messages, and then also add an error message for just this field.
Is there a way to hook into the save! with a callback?
I am looking for something like:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
before_save :save_subservices! if bang_save?
private
def save_subservices
#subservices.each(&:save)
end
def save_subservices!
#subservices.each(&:save!)
end
end
Where a save! is cascaded and calls save! on the (faux) association subservices.
Technically you can do this, but I would advise not to use this approach in production because it can change in newer rails. And it is just wrong.
You can inspect call stack of your before callback and check if there is save! method.
class CompositeService < Service
before_save :some_callback
def some_callback
lines = caller.select { |line| line =~ /persistence.rb/ && line =~ /save!/ }
if lines.any?
#subservices.each(&:save!)
else
#subservices.each(&:save)
end
end
end
I wonder: is this extra logic even necessary?
If the save method on each of your #subservices obeys the ActiveRecord save semantics, then you probably will get the correct behavior for free.
In other words, make sure your save methods return true or false for success or failure. Then, the composite code becomes as simple as this:
class CompositeService < Service
attr_accessible :subservices
before_save :save_subservices
private
def save_subservices
#subservices.all?(&:save)
end
end
If any of your sub services fail to save, then the save_subservices callback will return false, which will abort the callback chain. This will cause the wrapping save to return false. And in the case of save!, it will raise an exception.
composite.save
# => false
composite.save!
# => ActiveRecord::RecordNotSaved
Look at ActiveRecord autosave attribute:
http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
I have an Item object that has a boolean value called has_instore_image that when set in a before_save method must be set to 0 rather than false. Is there a reason why this is so? Is this the expected behavior? I'm using Rails 3.2.19.
code:
class Item < ActiveRecord::Base
attr_accessible :has_instore_image
before_save :set_has_instore_image
def set_has_instore_image
if self.instore_images.count>0
self.has_instore_image=true
else
self.has_instore_image=0
#self.has_instore_image=false
end
end
From the Rails docs on callbacks:
If a before_* callback returns false, all the later callbacks and the associated action are cancelled.
So what happens is that, as the last expression of your callback evaluates to false, then the callback chain is stopped and the save action fails.
You can remedy it like this:
def set_has_instore_image
if self.instore_images.count>0
self.has_instore_image=true
else
self.has_instore_image=false
end
true
end
In fact, it's considered good practice to end all your before_*callback definitions returning true to avoid this very same problem.