rails singular_collection_ids validation - ruby-on-rails

I have a form in rails app that allows to update singular_collection_ids attribute (relation type is has_many through). And I also need to validate it before update.
The problem that is validation requires previous value of object, but there is no method singular_collection_ids_was to provide this value. Also singular_collection_ids method works directly with join table with no temporary values, so
self.class.find(id).singular_collection_ids
inside validation did not help.
Is there any way to get previous value in stage of validation?

Not sure it works (and it's definitely quirky) but you could try this :
class Player
has_many :join_models, before_remove: :prevent_achievement_removal
def prevent_achievement_removal( join_model )
errors.add( :base, "Cannot remove an achievement this way !" )
raise ::ActiveRecord::RecordInvalid
end
end
according to the doc, if the callback raises any error, the record is not removed. However, something does not really feel right about this... I'll try to think about a better solution.

Related

Rails: validate number of associations

How do you write validations for a number of associations that is externally defined? I've so far written something like this:
class Document
validate :publication_count
private
def publication_count
if publications.count > template.component_count
errors.add(:articles, 'too many')
elsif publications.count < template.component_count
errors.add(:articles, 'not enough')
end
end
Both publications and template are associations. I just get a rollback error with this code, even though the record should be valid.
Your code appears correct, so it seems likely that the associations aren't being set or saved correctly.
Did you check that:
publications and template are both assigned to the Document instance before you save?
the rollback error isn't for a different reason, like uniqueness failure?
this is the actual validation that's failing rather than another one?

How can I validate that has_many relations are not changed

I have an fairly typical Order model, that has_many Lines
class Order < ActiveRecord::Base
has_many :lines
validates_associated :lines
Once the order is completed, it should not be possible to change any attributes, or related lines (though you can change the status to not completed).
validate do
if completed_at.nil? == false && completed_at_was.nil? == false
errors.add(:base, "You can't change once complete")
end
end
This works fine, but, if you add to, remove, or change the associated Lines, then this isn't prevented.
In my Line model, I have the following validation:
validate do
if order && order.completed_at.nil? == false
errors.add(:base, "Cannot change once order completed.")
end
end
This successfully stops lines in a completed order being modified, and prevents a line being added to a completed order.
So I need to also prevent lines being taken out of a completed order. I tried this in the Line model:
validate do
if order_id_was.nil? == false
if Order.find(order_id_was).completed_at.nil? == false
errors.add(:base, "Cannot change once order completed.")
end
end
end
This works fine to prevent a Line being taken out of an Order when modifying the Line directly. However when you are editing the Order and remove a Line, the validation never runs, as it has already been removed from the Order.
So... in short, how can I validate that the Lines associated with an Order do not change, and are not added to or removed?
I'm thinking I'm missing something obvious.
From the "Association Callbacks" section of ActiveRecord::Associations, you'll see that there are several callbacks that you can add to your has_many definition:
before_add
after_add
before_remove
after_remove
Also from the same docs:
Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with the before_remove callbacks; if an exception is thrown the object doesn't get removed.
Perhaps you can add a callback method to before_add and before_remove that makes sure the order isn't frozen and throws an exception if it's not allowed.
has_many :lines,
before_add: :validate_editable!,
before_remove: :validate_editable!
private
def validate_editable_lines!(line)
# Define the logic of how `editable?` works based on your requirements
raise ActiveRecord::RecordNotSaved unless editable?(line)
end
Another thing worth trying would be to add a validation error and return false within validate_editable_lines! if your validation test fails. If that works, I'd recommend changing the method name to validate_editable_lines (sans ! bang), of course. :)
This is an interesting problem, and to the best of my knowledge slightly tricky to solve.
Here is one approach: http://anti-pattern.com/dirty-associations-with-activerecord
Another approach which I think is slightly cleaner would be to simply check at the controller level before you add/remove a Line, and not to use validations.
Yet another approach is you can add before_create and before_destroy callbacks to Line, and check if the Order instance has been completed.
Maybe add a locked attribute to the model, and, after the order is completed set the value of locked to true.
Then, in the controller, add a before_filter that will be triggered before the update action so it would check the value of the locked flag. If it is set to true then raise an error/notification/whatever to the user that that line item cannot be changed.

How do I change an ActiveRecord from marked to be saved to make sure it does not get saved, from within the model itself?

How do I change an AcriveRecord from marked to be saved to make sure it does not get saved, from within the model itself?
Considering I can have a method run by a hook in activerecord, such as: before_save
for (hypothetical) example:
before_save :ignore_new_delete_exisiting_if_blank(self.attribute)
def ignore_new_delete_exisiting_if_blank(attribute)
self.do_not_save_me! if attribute.blank?
#what is that magic "do_not_save_me" method?
#Is there such thing, or something to achieve the same thing?
end
Update
My particular use case requires that no errors be thrown and other models to continue to be saved, even if this one will not. I should explain:
I am using model inheritance, and I am having an issue with figuring out how to let save the parent model, but if the child model instances are blank, (no values exist in certain attributes) they should not be persisted; however, the parent should still be persisted. This scenario does not let me make use of validations on the child model as that would block the parent from being persisted as well...
Your method should just return false to make it does not save.
Or you set the errors, which will allow to be more descriptive.
For example:
def ignore_new_delete_exisiting_if_blank_attribute
if attribute.blank?
errors.add(:base, "Not allowed to save if attribute is blank.")
end
end
Note that you cannot send parameters to a before_save. If you just want to make sure a record is not saved when an attribute is not present, you should use
validates_presence_of :attribute
[UPDATE]
When saving a parent model with children, you have to do something like accepts_nested_attributes_for, and in that call, you can specify which attributes must be given or when a child-record is ignored.
For example
accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
will not save a post if the title is blank.
Hope this helps.
The "magic" is that when you return false from the method, the record won't be saved.
In your case:
def ignore_new_delete_exisiting_if_blank(attribute)
attribute.present?
end

silently skip add with before_add association callback instead of raising an exception?

I'm trying to do this
has_many :roles, :before_add => :enforce_unique
def enforce_unique(assoc)
false if exists? assoc
end
From the docs: "If a before_add callback throws an exception, the object does not get added to the collection". The using false above does not prevent the add, so I'm forced to do this:
def enforce_unique(assoc)
raise if exists? assoc
end
This way, it's true that it doesn't get added, but it also raises an exception that has to be handled. Not very useful to me here. I would prefer this to behave more like regular AR callback before_save, where returning FALSE also prevents the save (or add) but doesn't raise an exception.
In this case above, I would prefer this to just not add the assoc silently. Is there a way to do this? I missing something? Or is raising an exception the only option here?
The way to solve it I think is to use throw and catch, which in Ruby are meant for flow control. Raising an exception is not a good fit, since this isn't an exceptional circumstance.
I ended up doing:
catch(:duplicate) do
association.create({})
end
And then in the before_add callback, I did:
if(Class.where({}).first)
throw :duplicate
end
More on throw/catch here:
http://rubylearning.com/blog/2011/07/12/throw-catch-raise-rescue-im-so-confused/
If the association isn't polymorphic you can do something like:
validates_uniqueness_of :name_of_model
inside of Role where name_of_model us what you are associating with
this question is a bit old, but i came across the same problem recently. here is how i solved it:
def enforce_unique |obj, x|
v = obj.roles
if i = v.index(x)
v.slice! i
end
end
Since this question is about saving rather than preventing it being included in the list temporarily (eg by a controller that is not interested in controlling its models) you could try overriding save in the related model and just not save it if the role exists:
class Role < ActiveRecord::Base
belongs_to :user, inverse_of: :roles
def save
super unless self.new_record? && user.has_existing_role?(self)
end
end
Sidenote: I don't buy the skinny controller argument when used with the Active Record pattern as business logic has to be put somewhere. With a business domain poor pattern like Active Record (not referring to the Ruby AR gem specifically) it really needs to exist a layer above (ie in the controller layer), you may use service objects or the decorator pattern as a means to achieve this.
Another approach would be to override update methods like << for the association and silently drop the role if it matches an existing one. Details on overriding association methods are in the ActiveRecord Association Class Methods Documentation

has_one relationship validation in rails

Since has_one doesn't provide a before_add callback to allow validation,
how do I prevent rails from destroying the old association even when the
new one does'nt pass validation?
susan :has_one :shirt
shirt :belongs_to :susan
susan.shirt = a_nice_shirt
this destroys whatever association was present beforehand,
even if the new shirt is never realy associated because it didn't pass
validation, leaving a shirtless susan behind (well, acutally leaving a shirt
behind that doesn't belong to anyone..).
susan.build_shirt
does the same thing
Is there a good reason for the missing before_add callback that I overlooked?
I'm not sure why that callback isn't there, but you can always just add an Observer to the model, and verify the new association in a before_save. I'll assume "susan" is a User Model instance, and the shirt has to be red to pass validation.
class UserObserver< ActiveRecord::Observer
def before_save(user)
return false if user.shirt.color != "red"
end
end
If you return false in the observer, the object won't save. Of course, your current instance of "susan" will still have the invalid association. I'm not positive, but if you change the before_save_ in the observer to something like this:
class UserObserver< ActiveRecord::Observer
def before_save(user)
if user.shirt.color != "red"
user.reload
false
end
end
Might refresh the instance of your User. I've never tried this though.
In Rails validation is usually done when you try to save the object to the database (using save or save!), not when it's modified. If for any reason you would like to restore the old value when the validation fails you can either reload the object or use the new dirty attributes feature.
Look at this ticket:
http://dev.rubyonrails.org/ticket/10518
It appears that the functionality described there is still present if you have :dependent => :destroy on your association.
Personally, I think this is a bug in rails.

Resources