Rails model validation which check if already exists object with some property - ruby-on-rails

I have following model:
class TestModel < ActiveRecord::Base
enum some_column: [:prop1, :prop2, :prop3]
end
I want to ensure i will always have maximum one object with prop1 column value in database, while i can have multiple numbers of object with prop2 or prop3 objects or none of them. It would be great if it could be a model validation. I tried this way but not sure if this is good practice for a rails app:
if ((id.nil? and TestModel.where(some_column: 'prop1')) or (TestModel.where(some_column: 'prop1').where.not(id: id)))
I tried with left side to cover create action, with right side to cover update. Is there any rails way to do this?
Thanks
UPDATED
This is solution for my problem
def only_one_prop1_record
if(some_column == 'prop1')
if new_record? #if create action
if TestModel.where(some_column: 'prop1').any?
errors.add(:base, 'You can only have one prop1 record')
end
elsif persisted? #if update action
if TestModel.where(some_column: 'prop1').where.not(id: id).any?
errors.add(:base, 'You can only have one prop1 record')
end
end
end
end
And call validation like this: validate :only_one_prop1_record

This might help you:
class TestModel < ActiveRecord::Base
enum some_column: [:prop1, :prop2, :prop3]
validate :only_one_prop1_record, on: :create
def only_one_prop1_record
if TestModel.exists?(some_column: prop1)
errors.add(:base, 'You can only have one prop1 record')
end
end
end
Or may be to run the validation on update and create both you can try this:
def only_one_prop1_record
existing_record = TestModel.where(some_column: prop1).first
if (new_record? && existing_record.present?) || (persisted? && existing_record != self)
errors.add(:base, 'You can only have one prop1 record')
end
end
And remove the on: :create from above.

Related

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

Object can not be deleted is attribute is active - Rspec RoR

I'm trying to test if object is active and it is active then it can not be deleted. So for i have this in my plan_spec.rb:
it "can not be deleted if active" do
plan = Plan.new(active: true)
r = plan.destroy
r.should_not be_valid
end
Name of the attribute i'm trying to check is 'active' and it is boolean, so if active is true then it can't object plan can not be deleted.
Any help?
It can be achieved by using before_destroy callback which will return false if record can't be destroyed:
class Plan < ActiveRecord::Base
# ...
before_destroy :check_if_active
# ...
private
def check_if_active
!active?
end
end
With this solution, you should also rewrite your test, cause you shouldn't check validity:
it 'can not be deleted if active' do
plan = Plan.create!(active: true)
expect { plan.destroy }.to_not change { Plan.count }
end
in your Plan class, add:
before_destroy :ensure_inactive
#will abort destroy if return false
def ensure_inactive
!active?
end
Btw, your spec is wrong, its not really a validation. You should:
save the object
call destroy
ensure it has not been destroyed

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

Conditional validations in Rails

I have in my model these two fields - recurring_interval and recurring_time - where I need to validate, that these two fields will be set up both or none of them.
If user will want to set up recurring, he will need to set up both these columns. But if he doesn't, he will leave these columns empty.
How to do that?
class User < ActiveRecord::Base
validates_with Recurring_validator
end
class RecurringValidator < ActiveModel::Validator
def validate(record)
if record.recurring_interval.blank? != record.recurring_time.blank?
record.errors[:base] << "Error message"
end
end
end
The advantage of this approach is, you can use the validator class for other models too.
You can add a custom validation:
class MyModel
...
validate :verify_recurring_stuff
..
private
def verify_recurring_stuff
ri = self.recurring_interval
rt = self.recurring_time
if ri.blank? != rt.blank?
self.errors.add(:base, 'Your error message')
end
end
end

What executes first?

In my rails app I have a User model.
In that model I have some custom validation and a before save block as below
Class User < AvtiveRecord::Base
before_save :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Now, I just wanted to know that whether the validate block executes first or the validate. Because there are other validations based on the user role. So if the validate block executes first and there are no validation errors and then the before save executes and modifies the values. Are those values again validated?
Thanks in Advance.
Validations are called before before_save callbacks. If you want it to execute before the validations then you can use before_validation_on_create or before_validation_on_update, like this:
class User < ActiveRecord::Base
before_validation_on_create :save_user
validate :validate_user
def save_user
self.guest = true if(!self.admin? && !self.guest)
end
def validate_user
errors.add(:age, "can't be less than 20") if self.age < 20
end
end
Those values will not be validated again. Validation happens once as does save, otherwise you could end up in a looping condition anytime you changed a value.
This is the guide you want: http://guides.rubyonrails.org/active_record_validations_callbacks.html
Based on section 10, it looks as if validation happens first.
I don't think the values will be validated again—there's nothing that would cause that to happen.

Resources