Colin
Colin

Reputation: 784

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.

Upvotes: 6

Views: 2980

Answers (3)

Chris Peters
Chris Peters

Reputation: 18090

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

Upvotes: 5

Ilija Eftimov
Ilija Eftimov

Reputation: 820

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.

Upvotes: 1

Benjamin Tan Wei Hao
Benjamin Tan Wei Hao

Reputation: 9691

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.

Upvotes: 1

Related Questions