Rails 3.2 Prevent Object from being Saved using Errors - ruby-on-rails

I have an ActiveRecord object and I would like to prevent it from being saved, without having permanent validations on the model. You used to be able to do something like this using errors.add but it doesn't look like it works anymore.
user = User.last
user.errors.add :name, "name doesn't rhyme with orange"
user.valid? # => true
user.save # => true
or
user = User.last
user.errors.add :base, "my unique error"
user.valid? # => true
user.save # => true
How can I prevent the user object from getting saved in Rails 3.2 without modifying it's model?

You can set errors, but do it within a validate method, e.g.:
validate :must_rhyme_with_orange
def must_rhyme_with_orange
unless rhymes_with_orange?
errors.add(:name, "doesn't rhyme with orange")
end
end
If you want to conditionally run the validation, one trick is to use attr_accessor and a guard condition:
attr_accessor :needs_rhyming
validate :must_rhyme_with_orange, :if => Proc.new {|o| o.needs_rhyming}
> u = User.last
> u.needs_rhyming = true
> u.valid? # false

Your issue is running valid? will rerun the validations.. resetting your errors.
pry(main)> u.errors[:base] << "This is some custom error message"
=> ["This is some custom error message"]
pry(main)> u.errors
=> {:base=>["This is some custom error message"]}
pry(main)> u.valid?
=> true
pry(main)> u.errors
=> {}
pry(main)>
Instead, just check if u.errors.blank?

This is a slight deviation from the original question, but I found this post after trying a few things. Rails has built in functionality to reject an object from saving if it has the _destroy attribute assigned as true. Quite helpful if you're handling model creation on the controller level.

Related

Ruby on Rails validation - If model is invalid, then?

I have an EmailContact with validation, like so:
class EmailContact < ActiveRecord::Base
validates :email, :presence => true, :email => {:message => I18n.t('validations.errors.models.user.invalid_email')},
:mx => {:message => I18n.t('validations.errors.models.user.invalid_mx')}
end
Here I am validating EmailContact.email.
Then I have a PhoneContact with no validation:
class PhoneContact < ActiveRecord::Base
end
I want to write something like this:
email_contact = EmailContact.create(params)
if email_contact.invalid?
phone_contact = PhoneContact.create(params)
end
Basically, if the email_contact can't be created due to validation errors, I should then create a phone_contact. How is that possible?
This is what I've tried:
contact = EmailContact.create(:email => 'a')
(0.3ms) BEGIN
(0.4ms) ROLLBACK
ArgumentError: cannot interpret as DNS name: nil
contact.invalid?
NoMethodError: undefined method `invalid?' for nil:NilClass
contact just returns nil in this case...
EDIT
It may be that this question needs to go a different direction. Just FYI:
email_contact = EmailContact.new(:email => 'a')
email_contact.valid?
ArgumentError: cannot interpret as DNS name: nil
email_contact.valid? returns an error instead of returning false as I would expect. I am using the valid_email gem to do my validation on the model.
Using invalid? method, you could do something like this :
email_contact = EmailContact.new(params)
if email_contact.invalid?
phone_contact = PhoneContact.create(params)
else
email_contact.save
end
In your case, you used email_contact = EmailContact.create(params).
email_contact would be set to nil if creation fails and you won't be able to call invalid? on a nil object.
Check for invalid? before creating the record in database.
You should use #new and then call #save directly.
email_contact = EmailContact.new(params)
if !email_contact.save
phone_contact = PhoneContact.create(params)
end

In Rails, how do I limit which attributes can be updated, without preventing them from being created?

I have a situation where an attribute can be created through a JSON API. But once it is created, I want to prevent it from ever being updated.
This constraint causes my first solution, which is using attr_accessible, to be insufficient. Is there a nice way to handle this type of situation in rails, or do I have to perform a manual check in the update method?
You can use attr_readonly, this will allow the value to be set on creation, but ignored on update.
Example:
class User < ActiveRecord::Base
attr_accessible :name
attr_readonly :name
end
> User.create(name: "lorem")
> u = User.first
=> #<User id: 1, name: "lorem">
> u.name = "ipsum"
=> "ipsum"
> u.save
=> true
> User.first.name
=> "lorem"
There is not a nice way to do that as far as I know, you have to write a custom filter
before_update :prevent_attributes_update
def prevent_attribute_updates
%w(attr1, attr2).each do |a|
send("#{attr1}=", send("#{attr1}_was")) unless self.send("#{attr1}_was").blank?
end
end

Rails model.valid? flushing custom errors and falsely returning true

I am trying to add a custom error to an instance of my User model, but when I call valid? it is wiping the custom errors and returning true.
[99] pry(main)> u.email = "test#test.com"
"test#test.com"
[100] pry(main)> u.status = 1
1
[101] pry(main)> u.valid?
true
[102] pry(main)> u.errors.add(:status, "must be YES or NO")
[
[0] "must be YES or NO"
]
[103] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={:status=>["must be YES or NO"]}>
[104] pry(main)> u.valid?
true
[105] pry(main)> u.errors
#<ActiveModel::Errors:[...]#messages={}>
If I use the validate method from within the model, then it works, but this specific validation is being added from within a different method (which requires params to be passed):
User
def do_something_with(arg1, arg2)
errors.add(:field, "etc") if arg1 != arg2
end
Because of the above, user.valid? is returning true even when that error is added to the instance.
In ActiveModel, valid? is defined as following:
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
errors.clear
run_validations!
ensure
self.validation_context = current_context
end
So existing errors are cleared is expected. You have to put all your custom validations into some validate callbacks. Like this:
validate :check_status
def check_status
errors.add(:status, "must be YES or NO") unless ['YES', 'NO'].include?(status)
end
If you want to force your model to show the errors you could do something as dirty as this:
your_object = YourModel.new
your_object.add(:your_field, "your message")
your_object.define_singleton_method(:valid?) { false }
# later on...
your_object.valid?
# => false
your_object.errors
# => {:your_field =>["your message"]}
The define_singleton_method method can override the .valid? behaviour.
This is not a replacement for using the provided validations/framework. However, in some exceptional scenarios, you want to gracefully return an errd model. I would only use this when other alternatives aren't possible. One of the few scenarios I have had to use this approach is inside of a service object creating a model where some portion of the create fails (like resolving a dependent entity). It doesn't make sense for our domain model to be responsible for this type of validation, so we don't store it there (which is why the service object is doing the creation in the first place). However for simplicity of the API design it can be convenient to hang a domain error like 'associated entity foo not found' and return via the normal rails 422/unprocessible entity flow.
class ModelWithErrors
def self.new(*errors)
Module.new do
define_method(:valid?) { false }
define_method(:invalid?) { true }
define_method(:errors) do
errors.each_slice(2).with_object(ActiveModel::Errors.new(self)) do |(name, message), errs|
errs.add(name, message)
end
end
end
end
end
Use as some_instance.extend(ModelWithErrors.new(:name, "is gibberish", :height, "is nonsense")
create new concerns
app/models/concerns/static_error.rb
module StaticError
extend ActiveSupport::Concern
included do
validate :check_static_errors
end
def add_static_error(*args)
#static_errors = [] if #static_errors.nil?
#static_errors << args
true
end
def clear_static_error
#static_errors = nil
end
private
def check_static_errors
#static_errors&.each do |error|
errors.add(*error)
end
end
end
include the model
class Model < ApplicationRecord
include StaticError
end
model = Model.new
model.add_static_error(:base, "STATIC ERROR")
model.valid? #=> false
model.errors.messages #=> {:base=>["STATIC ERROR"]}
A clean way to achieve your needs is contexts, but if you want a quick fix, do:
#in your model
attr_accessor :with_foo_validation
validate :foo_validation, if: :with_foo_validation
def foo_validation
#code
end
#where you need it
your_object.with_foo_validation = true
your_object.valid?

Is there a way to bypass mass assignment protection?

I have a Rails 3 app which JSON encodes objects in order to store them in a Redis key/value store.
When I retrieve the objects, I'm trying to decode the JSON and instantiate them from the data like so:
def decode(json)
self.new(ActiveSupport::JSON.decode(json)["#{self.name.downcase}"])
end
The problem is that doing this involves mass assignment which is disallowed (for good reason I'm told!) for attributes I haven't given attr_writer ability to.
Is there a way I can bypass the mass assignment protection just for this operation only?
assign_attributes with without_protection: true seems less intrusive:
user = User.new
user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
user.name # => "Josh"
user.is_admin? # => true
#tovodeverett mentioned in the comment you can also use it with new, like this in 1 line
user = User.new({ :name => 'Josh', :is_admin => true }, :without_protection => true)
EDIT: kizzx2's Answer is a much better solution.
Kind of a hack, but...
self.new do |n|
n.send "attributes=", JSON.decode( json )["#{self.name.downcase}"], false
end
This invokes attributes= passing false for the guard_protected_attributes parameter which will skip any mass assignment checks.
You can create a user also in this way which is not doing the mass assignment.
User.create do |user|
user.name = "Josh"
end
You may want to put this into a method.
new_user(name)
User.create do |user|
user.name = name
end
end

RoR: why does this validation fail?

This one really has me. I have this validation in my user model:
validates :first_class, :presence => true, :inclusion => %w(Fighter Ranger Magician)
Now, I try an example create in my console:
ruby-1.9.2-p180 :053 > new = User.create(:first_class => 'Magician')
=> #<User id: nil, ...
ruby-1.9.2-p180 :054 > new.errors
=> {:first_class=>["can't be blank", "is not included in the list"]}
Why am I getting this validation error? I SERIOUSLY cannot figure that out.
(If i remove the validation, the user gets created, but first_class is nil :O)
maybe try having attr_accessible :first_class in your model file
You have to tell rails which attributes are writeable through mass-assignment. The new method takes a parameters hash, which is considered mass-assignment. The same is true with update_attributes.
To verify, you could just make a new instance and say object.first_class = 'Magician'. If this also fails, then you know attr_accessible is not the problem.

Resources