ActiveRecord validation by parent attribute - ruby-on-rails

I have model Article, for example. Article has_one ArticleContent. ArticleContent has validation of all it's attributes by default. But I need additional functionality - to save draft Article, without any validation.
So I pass :draft => false as one of a parameter in Article.new(), next I do #article.build_article_content(). There is some not working code in ArticleContent:
def draft?
raise self.article.draft
end
validates_presence_of :header, :message => "We have no fuckin' header!", :unless => :draft?
Of course it's not work. At the moment of draft? execution there is no any suitable Article object anywhere, so self.article returns nil. Nice try, codemonkey...
Anyone have some sweet ideas? I think to make #content.save! is not a very good idea
UPDATE
I tried so:
def draft
self[:draft]
end
def draft=(value)
self[:draft] = value
end
def draft?
self[:draft]
end
validates_presence_of :field1, :message => "msg1", :unless => :draft?
validates_presence_of :field2, :message => "msg2", :unless => :draft?
validates_presence_of :field3, :message => "msg3", :unless => :draft?
It works, but how can I group this?
unless self.draft?
validates_presence_of :field1, :message => "msg1"
validates_presence_of :field2, :message => "msg2"
validates_presence_of :field3, :message => "msg3"
end
Says that draft? method is not found. Also i should do
#article.content.draft = #article.draft
And it looks like dirty-dirty hack too

This is a common use case for a state machine. There are several rails plugins that provide for those.
http://ruby-toolbox.com/categories/state_machines.html
If you don't need a full state machine implementation it could still be instructive to have a state column in your ArticleContent model. Its values would be "new", "draft", "published" and so on. Your validations would look at that column's value when deciding what to do, like:
validates :content, :presence => true, :unless => Proc.new { |a| a.state == "Draft" }
(I'm pretty sure that's not the correct syntax but you should get what I'm aiming at.)
To answer your UPDATE
Try with_options.
with_options :unless => :draft? do |o|
o.validates_presence_of :field1, :message => "msg1"
o.validates_presence_of :field2, :message => "msg2"
o.validates_presence_of :field3, :message => "msg3"
end
Looking at your code there's a couple of smells. In order to flunk a validation the thing to do is errors.add(blah), not raise an exception. Also, your methods defined for accessing the draft column look a little redundant. They're just doing what AR would do anyway.

Related

Rails 3 - how to skip validation rule?

I have for the registration form this validation rule:
validates :email,
:presence => {:message => 'cannot be blank.'},
:allow_blank => true,
:format => {
:with => /\A[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\.[A-Za-z]+\z/,
:message => 'address is not valid. Please, fix it.'
},
:uniqueness => true
This rule check, if a user fill into the registration form email address (+ its correct format).
Now I am trying to add the opportunity to log in with using Twitter. Twitter doesn't provide user's email address.
How to skip in this case the validation rule above?
You can skip validation while saving the user in your code. Instead of using user.save!, use user.save(:validate => false). Learnt this trick from Railscasts episode on Omniauth
I'm not sure whether my answer is correct, just trying to help.
I think you can take help from this question. If i modify the accepted answer for your question, it will be like (DISCLAIMER: I could not test the following codes as env is not ready in the computer i'm working now)
validates :email,
:presence => {:message => 'cannot be blank.', :if => :email_required? },
:allow_blank => true,
:format => {
:with => /\A[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\.[A-Za-z]+\z/,
:message => 'address is not valid. Please, fix it.'
},
:uniqueness => true
def email_required?
#logic here
end
Now, you update the email_required? method to determine whether it is from twitter or not! If from twitter, return false otherwise true.
I believe, you need use same :if for the :uniqueness validator too. otherwise it will. Though, i'm not sure too :(. Sorry
Skipping Individual Validations
Skipping individual validations requires a bit more work. We need to create a property on our model called something like skip_activation_price_validation:
class Product < ActiveRecord::Base
attr_accessor :skip_activation_price_validation
validates_numericality_of :activation_price, :greater_than => 0.0, unless: :skip_activation_price_validation
end
Next we will set the property to true any time we want to skip validations. For example:
def create
#product = Product.new(product_params)
#product.skip_name_validation = true
if #product.save
redirect_to products_path, notice: "#{#product.name} has been created."
else
render 'new'
end
end
def update
#product = Product.find(params[:id])
#product.attributes = product_params
#product.skip_price_validation = true
if #product.save
redirect_to products_path, notice: "The product \"#{#product.name}\" has been updated. "
else
render 'edit'
end
end
You seem to be doing two separate validations here:
If a user provides an email address, validate it's format and uniqueness
Validate the presence of an email address, unless it's a twitter signup
I would do this as two separate validations:
validates :email,
:presence => {:message => "..."},
:if => Proc.new {|user| user.email.blank? && !user.is_twitter_signup?}
validates :email,
:email => true, # You could use your :format argument here
:uniqueness => { :case_sensitive => false }
:unless => Proc.new {|user| user.email.blank?}
Additional info: validate email format only if not blank Rails 3
The best way would be:
It would validate the email when the user is not signed in from twitter as well as skip email validation when signed from twitter.
validates :email,
:presence => {:message => 'cannot be blank.'},
:allow_blank => true,
:format => {
:with => /\A[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\.[A-Za-z]+\z/,
:message => 'address is not valid. Please, fix it.'
},
:uniqueness => true
unless: Proc.new {|user| user.is_twitter_signup?}

ActiveRecord validate :uniqueness on association

I need to perform the validation to make sure that only one user within a company can exist within a given category.
validates :user_id, :uniqueness => {:scope => [:category, :company_id], :message => "already exists"}
This works except the error message is set on :user_id key.
How can I do the same but set the error on the :user key (validates :user gives an error)?
Here's a simple way to check uniqueness and force the error to be assigned to the :user attribute:
class User < ActiveRecord::Base
validate :user_unique_per_company_per_category
private
def user_unique_per_company_per_category
if self.class.exists?(:user_id => user_id, :company_id => company_id, :category => category)
errors.add :user, 'already exists'
end
end
end
It would be preferable, I think, if you could figure out a way to use the default validation on :user_id, but maybe you have a special use case.
Also, if you're not using this in a form, you might consider assigning the error to :base, since you might confuse future developers who expect the error to appear on :user_id:
errors.add :base, 'already exists'
I don't think this is possible as the validates method of ActiveRecord sends the errors to the method being validated.
So validates :user trys to send to the attr_accessor of :user which doesn't exist in your model.
Though, if you're just trying to make the error message pretty you can:
alias user user_id
And then use :user in your validation.
validates :user, :uniqueness => {:scope => [:category, :company_id], :message => "already exists"}
On a side note, I wouldn't use user in the alias rather something like:
alias the_supplied_user user_id
And then in your validation:
validates :the_supplied_user, :uniqueness => {:scope => [:category, :company_id], :message => "already exists"}

How can I combine multiple Rails validations for the same :if condition?

I have a model with many validations that can be grouped based on various conditions. The brute force way to handle it would be:
validates_presence_of :attr1, :if => :condition1
validates_something :attr2, :if => :condition1
validates_something_else :attr3, :if => :condition1
...
validates_presence_of :attr4, :if => :condition2
validates_something :attr5, :if => :condition2
validates_presence_of :attr6, :if => :condition2
...
But that doesn't seem very DRY. Is there a good way to group the validations based on the conditions? The approach I came up with is:
class Condition1Validator < ActiveModel::Validator
def validate(record)
record.instance_eval do
validates_presence_of :attr1
validates_something, :attr2
validates_something_else :attr3
end
end
end
validates_with Condition1Validator, :if => :condition1
class Condition2Validator < ActiveModel::Validator
...
end
validates_with Condition2Validator, :if => :condition2
Can anyone think of a better way?
Update: the way I posted above is flawed in that you cannot have if, unless, etc on the nested validators. Jesse's solution is much better.
This approach is from the multi-step wizard, where you only want to validate if you are on that wizard step. Should work for you as well
class YourModel
with_options :if => lambda { |o| o.whatever == "whatever" } do |on_condition|
on_condition.validates_presence_of :address
on_condition.validates_presence_of :city
end
with_options :if => lambda { |o| o.condition_the_second == "whatever" } do |on_condition|
on_condition.validates_presence_of :foo
on_condition.validates_presence_of :bar
end
end

validates_uniqueness_of can't check on unsaved data?

I have a model called Science Subject Choice
class ScienceSubjectChoice < SubjectChoice
belongs_to :subject
belongs_to :subject_preference
validates_associated :subject
validates_associated :subject_preference
#TODO: validation
validates :priority, :presence => true, :numericality => true, :inclusion => {:in => 1..SubjectPreference::MAX_SCIENCE_SUBJECT_CHOICE}
validates_uniqueness_of :subject_id, :scope => :subject_preference_id
validates_uniqueness_of :priority, :scope => :subject_preference_id
end
the uniqueness validator don't work on unsaved data?
How can I solve it?
Solution:
Instead of validating in itself, the parent object should do the validation:
def validate_science_subject_choices_uniqueness
if science_subject_choices.map(&:priority) != science_subject_choices.map(&:priority).uniq
errors[:base] << "Duplicated priority in science subject"
end
end
Validations do not work like that. They are dynamic by nature. If you want database constraints, you have to specify it in your migrations. For instance, a :uniq => true would make sure that a value is unique in your model.

How to update attributes without validation

I've got a model with its validations, and I found out that I can't update an attribute without validating the object before.
I already tried to add on => :create syntax at the end of each validation line, but I got the same results.
My announcement model have the following validations:
validates_presence_of :title
validates_presence_of :description
validates_presence_of :announcement_type_id
validate :validates_publication_date
validate :validates_start_date
validate :validates_start_end_dates
validate :validates_category
validate :validates_province
validates_length_of :title, :in => 6..255, :on => :save
validates_length_of :subtitle, :in => 0..255, :on => :save
validates_length_of :subtitle, :in => 0..255, :on => :save
validates_length_of :place, :in => 0..50, :on => :save
validates_numericality_of :vacants, :greater_than_or_equal_to => 0, :only_integer => true
validates_numericality_of :price, :greater_than_or_equal_to => 0, :only_integer => true
My rake task does the following:
task :announcements_expiration => :environment do
announcements = Announcement.expired
announcements.each do |a|
#Gets the user that owns the announcement
user = User.find(a.user_id)
puts a.title + '...'
a.state = 'deactivated'
if a.update_attributes(:state => a.state)
puts 'state changed to deactivated'
else
a.errors.each do |e|
puts e
end
end
end
This throws all the validation exceptions for that model, in the output.
Does anybody how to update an attribute without validating the model?
You can do something like:
object.attribute = value
object.save(:validate => false)
USE update_attribute instead of update_attributes
Updates a single attribute and saves the record without going through the normal validation procedure.
if a.update_attribute('state', a.state)
Note:- 'update_attribute' update only one attribute at a time from the code given in question i think it will work for you.
try using
#record.assign_attributes({ ... })
#record.save(validate: false)
works for me
Yo can use:
a.update_column :state, a.state
Check: http://apidock.com/rails/ActiveRecord/Persistence/update_column
Updates a single attribute of an object, without calling save.
All the validation from model are skipped when we use validate: false
user = User.new(....)
user.save(validate: false)
Shouldn't that be
validates_length_of :title, :in => 6..255, :on => :create
so it only works during create?

Resources