views:

521

answers:

3

I am creating a Category model and using the awesome_nested_set plugin (a replacement for acts_as_nested_set) to deal with the hierarchy. With awesome_nested_set, objects are created, then saved, and then placed within the set. As well, lft, rgt and parent_id are attr_protected so they cannot be written to directly.

I am running into two situations when placing the node into the set that I want to be able to catch so that I notify the user (there might be more that I haven't thought of yet):

  1. A node is attempted to be placed as its own child (self.id == self.parent_id)
  2. A node is attempted to be moved beneath its own descendant (self.descendants.include? self.parent_id == true)

In both cases, the move will fail, but awesome_nested_set will only raise an ActiveRecord::ActiveRecordError exception, with a message that is not as descriptive as I'd like to be able to give the user.

awesome_nested_set has a number of node moving methods, which all call move_to(target, position) (where position is one of :root, :child, :left or :right and target is the related node for all positions but :root). The method fires a before_move callback, but doesn't provide a way that I can see to validate a move before it happens. In order to validate a move, I'd need access to the target and position, which the callback does not receive.

Does anyone know of either a way to validate a move in awesome_nested_set (either by having a way to pass target and position to the before_move callback of by another method), or another nested set plugin that will let me validate? I'd prefer not to fork or write my own plugin.

A: 

I'm having the exact same problem with awesome_nested_set: how to validate a node move before it happens, so I have the chance to cancel the move operation if it's invalid, and to show the user a descriptive error.

It's been 3 weeks from when you asked the question, did you arrive to a good solution?

I haven't yet, unfortunately.
Daniel Vandersluis
A: 

That's right, I haven't found a "ruby way" to add this validation neither, without modifying the plugin source code. I think I will implement a similar solution as yours, but splitting the validation and the actual operation of adding to the nested_set in two methods. And those methods implemented in the model, not in the controller. That way you can reuse it from other controllers, you can test in isolation from the controller, etc.

+1  A: 

Here is the solution I came up with:

class Category < ActiveRecord::Base
  acts_as_nested_set :dependent => :destroy

  #=== Nested set methods ===

  def save_with_place_in_set(parent_id = nil)
    Category.transaction do
      return false if !save_without_place_in_set
      raise ActiveRecord::Rollback if !validate_move parent_id

      place_in_nested_set parent_id
      return true
    end

    return false
  end

  alias_method_chain :save, :place_in_set

  def validate_move(parent_id)
    raise ActiveRecord::RecordNotSaved, "record must be saved before moved into the nested set" if new_record?
    return true if parent_id.nil?

    parent_id = parent_id.to_i

    if self.id == parent_id
      @error = :cannot_be_child_of_self
    elsif !Category.all.map(&:id).include?(parent_id)
      @error = :given_parent_is_invalid
    elsif descendants.map(&:id).include? parent_id
      @error = :cannot_be_child_of_descendant
    end

    errors.add(:parent_id, @error) if @error
    return @error.nil?
  end

  def place_in_nested_set(parent_id)
    if parent_id.nil? || parent_id.blank?
      move_to_root
    else
      move_to_child_of parent_id
    end
    return true
  end
end

Now, in the controller, I just need to say @category.save(parent_id), where parent_id is nil or the ID of the parent, and the validation, node placement, and save is handled in the model.

Daniel Vandersluis