Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 554
Colin Avatar asked Sep 11 '13 16:09

Colin


2 Answers

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. :)

like image 193
Chris Peters Avatar answered Sep 18 '22 10:09

Chris Peters


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.

like image 31
Ilija Eftimov Avatar answered Sep 18 '22 10:09

Ilija Eftimov