Rails / Trees

From WhyNotWiki
< Rails
Revision as of 02:06, 11 October 2007 by Tyler (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

Rails / Trees  edit   (Category  edit)


Contents

[Model-level (category)]

Comparison: Features and best-suitedness

Support for ordered trees? Efficient to query for all descendants of a node? Comments / Overall impression
ActsAsTree No! No. You can pass :order => :position to read in a certain order, but doesn't help with changing/setting order/position.
ActsAsTree + ActsAsList Somewhat.
ActsAsOrderedTree  ?
ActsAsNestedSet Yes! Yes!
BetterNestedSet Yes! Yes!

Tests/Demonstrations!

See the tests I've started to write, which not only test for expected behavior and compatibility between different configurations/plugins, but also demonstrates somewhat how to use these plugins:

http://svn.tylerrick.com/public/rails/examples/trees/ordered_trees/test/acts_as_list_test.rb

I admit, it could be more extensive, ... but it's a start.

Comparison: Columns required/used'

ActsAsTree ActsAsList BetterNestedSet
parent_id -
position - -
lft/rgt - -

Comparison: How to...

ActsAsTree ActsAsTree + ActsAsList BetterNestedSet
Move node to different parent: n1.parent = n2; n1.save n1.parent = n2; n1.save? n1.move_to_child_of n2
Create a new node n2 a child of n1:
n1.children.create
same
n2 = Node.create
n2.move_to_child_of n1

star_full.gif star_full.gif star_empty.gif ActsAsTree

(acts_as_tree)

Categories/Tags: [Trees (category)] [acts_as plugins (category)]
Documentation: API Wiki
Source code: (Core Rails)








Examples

http://api.rubyonrails.org/classes/ActiveRecord/Acts/Tree/ClassMethods.html

  root      = Category.create("name" => "root")
  child1    = root.children.create("name" => "child1")
  subchild1 = child1.children.create("name" => "subchild1")

  root.parent   # => nil
  child1.parent # => root
  root.children # => [child1]
  root.children.first.children.first # => subchild1

http://wiki.rubyonrails.org/rails/pages/ActsAsTree

top = TreeItem.find_by_parent_id(nil)
top.children.each do |child|
  puts child.parent.name + " is my parent. I am " + child.name
end

Recursive find / Descendants / Children and children's children

http://wiki.rubyonrails.com/rails/pages/QuestionOfTheDay

[Paraphrase (category)]
Let's say you have a category model that acts_as_tree so that you can have a nice tree of categories. Let's also say that our category has_many products. So a category can have many categories and/or products. Given these two relationships, is there an easy way to get the products from a category AND the products from all its children and their children? So far this is what I am doing, just wondering if there is a better way:

products = []              
products += @category.products.find_all
@category.children.each {|child| products += child.products.find_all}

The problem with this is that it only searches one level of children, not children of children recursively.

Proposed better way:

category_ids = @category.children.collect{|x| x.id} << @category.id
products = Product.find :all, :conditions => ['category_id in (?)', category_ids]

The problem with this is that it only searches one level of children, not children of children recursively.

Also check out acts_as_threaded plugin...

acts_as_ordered_tree also tries to solve this, using its descendants method.

http://www.rubyinside.com/19-rails-tricks-most-rails-coders-dont-know-131.html

acts_as_nested_set - Almost everyone is familiar with [acts_as_tree, but acts_as_nested_set snuck into Rails quietly. It's much like acts_as_tree, but with the added benefit that you can select all of the children (and their own descendants) of a node with a single query. A list of the instance methods is available.

Test coverage

./test/fixtures/mixin.rb

class TreeMixin < Mixin
    acts_as_tree :foreign_key => "parent_id", :order => "id"
end

class TreeMixinWithoutOrder < Mixin
    acts_as_tree :foreign_key => "parent_id"
end

class RecursivelyCascadedTreeMixin < Mixin
  acts_as_tree :foreign_key => "parent_id"
  has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
end

test/fixtures/mixins.yml

tree_1:
  id: 1001
  type: TreeMixin
  parent_id:

tree_2:
  id: 1002
  type: TreeMixin
  parent_id: 1001

tree_3:
  id: 1003
  type: TreeMixin
  parent_id: 1002

tree_4:
  id: 1004
  type: TreeMixin
  parent_id: 1001

tree2_1:
  id: 1005
  type: TreeMixin
  parent_id:

tree3_1:
  id: 1006
  type: TreeMixin
  parent_id:

tree_without_order_1:
  id: 1101
  type: TreeMixinWithoutOrder
  parent_id:

tree_without_order_2:
  id: 1100
  type: TreeMixinWithoutOrder
  parent_id:

recursively_cascaded_tree_1:
  id: 5005
  type: RecursivelyCascadedTreeMixin
  parent_id:

recursively_cascaded_tree_2:
  id: 5006
  type: RecursivelyCascadedTreeMixin
  parent_id: 5005

recursively_cascaded_tree_3:
  id: 5007
  type: RecursivelyCascadedTreeMixin
  parent_id: 5006

recursively_cascaded_tree_4:
  id: 5008
  type: RecursivelyCascadedTreeMixin
  parent_id: 5007

./test/associations/cascaded_eager_loading_test.rb

  def test_eager_association_loading_with_acts_as_tree
    roots = TreeMixin.find(:all, :include=>"children", :conditions=>"mixins.parent_id IS NULL", :order=>"mixins.id")
    assert_equal [mixins(:tree_1), mixins(:tree2_1), mixins(:tree3_1)], roots
    assert_no_queries do
      assert_equal 2, roots[0].children.size
      assert_equal 0, roots[1].children.size
      assert_equal 0, roots[2].children.size
    end
  end

  def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
    root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:children=>{:children=>:children}}, :order => 'mixins.id')
    assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.children.first.children.first.children.first }
  end

  def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
    root_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:first_child=>{:first_child=>:first_child}}, :order => 'mixins.id')
    assert_equal mixins(:recursively_cascaded_tree_4), assert_no_queries { root_node.first_child.first_child.first_child }
  end

  def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
    leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include=>{:parent=>{:parent=>:parent}}, :order => 'mixins.id DESC')
    assert_equal mixins(:recursively_cascaded_tree_1), assert_no_queries { leaf_node.parent.parent.parent }
  end


star_full.gif star_full.gif star_empty.gif ActsAsTree + ActsAsList

  acts_as_tree :order => :position
  acts_as_list :scope => :parent_id


ActsAsOrderedTree

(acts_as_ordered_tree)


Categories/Tags: [Trees (category)] [acts_as plugins (category)]
Homepage: http://ordered-tree.rubyforge.org/
Documentation: http://ordered-tree.rubyforge.org/

There is a drag-n-drop demo (download): svn://rubyforge.org/var/svn/ordered-tree/demo

Source code: svn://rubyforge.org/var/svn/ordered-tree/acts_as_ordered_tree


As listed in other directories: http://agilewebdevelopment.com/plugins/acts_as_ordered_tree
Description: Adds list capability to the standard acts_as_tree, as well as various movements within the tree.





http://ordered-tree.rubyforge.org/

                                 +----+-----------+----------+---------+
  node_1                         | id | parent_id | position | node    |
    \_ node_2                    +----+-----------+----------+---------+
    \_ node_3                    |  1 |         0 |        1 | Node_1  |
    |    \_ node_4               |  2 |         1 |        1 | Node_2  |
    |    \_ node_5               |  3 |         1 |        2 | Node_3  |
    |    |   \_ node_8           |  4 |         3 |        1 | Node_4  |
    |    |   \_ node_9           |  5 |         3 |        2 | Node_5  |
    |    \_ node_10              |  6 |         1 |        3 | Node_6  |
    |    \_ node_11              |  7 |         1 |        4 | Node_7  |
    \_ node_6                    |  8 |         5 |        1 | Node_8  |
    \_ node_7                    |  9 |         5 |        2 | Node_9  |
...


     Actions    Tree Methods                                List Method
 --------------------------------------------------------------------------------------------------
     Create
                To create a child object at a specific position,
                use one of the following:
                  Person.create(:parent_id => parent.id, :position => 2)
                  parent.children << Person.new(:position => 3)
                  parent.children.create(:position => 5)

                To create a new 'root', use:
                  Person.create(:position => 2)

                :position will default to the bottom of the parent's list
                :parent_id defaults to 0 (Class.roots)

     Read
                roots (class method)                        self_and_siblings
                root                                        siblings
                parent                                      position_in_list
                ancestors
                children
                descendants

     Update
                shift_to(parent = nil, position = nil)      move_above(sibling = nil)
                orphan                                      move_higher
                orphan_children                             move_lower
                parent_adopts_children                      move_to_top


acts_as_tree / acts_as_ordered_tree comparison

ActsAsTree ActsAsOrderedTree
Location of source: /usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/acts/tree.rb (89 lines) ./vendor/plugins/acts_as_ordered_tree/lib/acts_as_ordered_tree.rb (439 lines)
Test coverage: There aren't many tests for ActsAsTree. Those that there are are mixed in among other ActiveRecord tests. The unique methods are pretty well tested, but the basic stuff, the stuff that is common with ActsAsTree, is not well tested at all.
This test:
  def test_root
    me = Person.create(:name => "Me")
    assert_equal nil, me.parent_id  # Fails for acts_as_ordered_tree (is 0) because their schema makes it a non-null, default 0 column
  end
passes fails
This test:
  def test_children_without_saving
    Person.delete_all
    dad =                     Person.new(:name => "Dad")
    dad.children << me =      Person.new(:name => "Me")
    assert_equal 1, dad.children.size  # Fails when using acts_as_ordered_tree! Because it tries to do a reload from the database, but in this case, the model only exists in memory, so the reload actually causes dad.children to be reset to [].
  end
passes fails
./vendor/plugins/acts_as_ordered_tree/lib/acts_as_ordered_tree.rb:
        def children(reload=false)
          reload = true if !@children
          reload ? child_nodes(true) : @children
        end

Conclusion: I do not recommend this plugin.

 


ActsAsNestedSet

(acts_as_nested_set)

(Actually use BetterNestedSet (see below), but they're mostly compatible so what you learn about one should be transferable to the other.)

[1]


I had set up an ActiveRecord for a hierarchical list of product categories. Since I wanted to be able to search arbitrarily within a category (i.e., “in this category OR any of its descendants), a nested set was the obvious choice [...].

Modeling a Threaded Forum with acts_as_nested_set

O'Reilly - Safari Books Online - 9780596527310 - Rails Cookbook (http://safari.oreilly.com/9780596527310/model_a_threaded_forum_with_acts_as_nested_set). Retrieved on 2007-05-11 11:18. star_full.gif star_full.gif star_empty.gif


You want to create a simple threaded discussion forum that stores all its posts in a single table. All posts should be visible in a single view, organized by topic thread. ...


ActsAsNestedSet + ActsAsList

Using ActsAsList and ActsAsNestedSet (http://wiki.rubyonrails.org/rails/pages/Using+ActsAsList+and+ActsAsNestedSet). Retrieved on 2007-05-11 11:18.


In certain situations, you might want to create a table that has the ordering properties of a nested set, without the overhead involved in keeping a nested set properly ordered. After all, you think, surely it doesn’t matter if the records are ordered correctly in the database, as long as the hierarchy is sound? I can use another column for the position attribute, and save my DBMS some work! Well, almost. This got me into a lot of trouble recently. I had set up an ActiveRecord for a hierarchical list of product categories. Since I wanted to be able to search arbitrarily within a category (i.e., “in this category OR any of its descendents), a nested set was the obvious choice, but I also wanted to give the user control of the order of sub-categories. No problem! I’d just declare the record as both acts_as_list and acts_as_nested_set. Since I wanted the positioning of the sub-categories to be local to their parent, I set the :scope parameter in the acts_as_list declaration to "parent_id". So that all worked, and I was testing out my system, when I noticed that for certain cases (notably adding sub-sub categories), the left and right limits weren’t getting updated properly in the database. What was going on? Eventually, I worked out that the index updating was stopping at the parent of the new category. But why? It turns out that acts_as_list and acts_as_nested_set both access a method called scope_condition. Since this was set by the acts_as_list declaration, it was getting picked up by acts_as_nested_set as well, even though that scope condition made no sense as far as the nested set updates were concerned (and turned out to be quite destructive). The Solution In the end, I did things the hard way: I got rid of the acts_as_list declaration and rewrote some of the instance methods to cope with a nested set implementation: ...


BetterNestedSet

BetterNestedSet edit


Documentation: .
Source code: http://opensource.symetrie.com/svn/better_nested_set/trunk
Project/Development: http://opensource.symetrie.com/trac/better_nested_set/
As listed in other directories: http://www.agilewebdevelopment.com/plugins/betternestedset


Based on: ActsAsNestedSet
License: MIT


Authors: Jean-Christophe Michel


http://opensource.symetrie.com/svn/better_nested_set/trunk/README

This plugin provides an enhanced acts_as_nested_set mixin for ActiveRecord, the object-db mapping layer of the framework RubyOnRails. The original nested set feature seems to be quite old and missed some necessary functionalities. Provides Nested Set functionality. Nested Set is a smart way to implement an ordered tree, with the added feature that you can select the children and all of their descendants with a single query. Nested sets are appropriate each time you want either an ordered tree (menus, commercial categories) or an efficient way of querying big trees (threaded posts). == Small nested set theory reminder Instead of picturing a leaf node structure with children pointing back to their parent, the best way to imagine how this works is to think of the parent entity surrounding all of its children, and its parent surrounding it, etc. Assuming that they are lined up horizontally, we store the left and right boundries in the database. Imagine:

  root
    |_ Child 1
      |_ Child 1.1
      |_ Child 1.2
    |_ Child 2
      |_ Child 2.1
      |_ Child 2.2

If my cirlces in circles description didn't make sense, check out this sweet ASCII art:

    ___________________________________________________________________
   |  Root                                                             |
   |    ____________________________    ____________________________   |
   |   |  Child 1                  |   |  Child 2                  |   |
   |   |   __________   _________  |   |   __________   _________  |   |
   |   |  |  C 1.1  |  |  C 1.2 |  |   |  |  C 2.1  |  |  C 2.2 |  |   |
   1   2  3_________4  5________6  7   8  9_________10 11_______12 13  14
   |   |___________________________|   |___________________________|   |
   |___________________________________________________________________|

The numbers represent the left and right boundries. The table then might look like this:

   id | parent_is | left | right | data
    1 |           |    1 |    14 | root
    2 |         1 |    2 |     7 | Child 1
    3 |         2 |    3 |     4 | Child 1.1
    4 |         2 |    5 |     6 | Child 1.2
    5 |         1 |    8 |    13 | Child 2
    6 |         5 |    9 |    10 | Child 2.1
    7 |         5 |   11 |    12 | Child 2.2

So, to get all children of an entry 'parent', you

    SELECT * WHERE left IS BETWEEN parent.left AND parent.right

To get the count, it's (right - left - 1)/2, etc. To get the direct parent, it falls back to using the parent_id field. There are instance methods for all of these.


API

Methods names are aligned on Tree's ones as much as possible, to make replacement from one by another easier, except for the creation:

in acts_as_tree:

  item.children.create(:name => "child1")

in acts_as_nested_set:

  # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
  child = MyClass.new(:name => "child1")
  child.save
  # now move the item to its right place
  child.move_to_child_of my_item

You can use:

  • move_to_child_of
  • move_to_right_of
  • move_to_left_of

and pass them an id or an object.

Other methods added by this mixin are:

  • root - root item of the tree (the one that has a nil parent; should have left_column = 1 too)
  • roots - root items, in case of multiple roots (the ones that have a nil parent)
  • level - number indicating the level, a root being level 0
  • ancestors - array of all parents, with root as first item
  • self_and_ancestors - array of all parents and self
  • siblings - array of all siblings, that are the items sharing the same parent and level
  • self_and_siblings - array of itself and all siblings
  • children_count - count of all immediate children
  • children - array of all immediate childrens
  • all_children - array of all children and nested children
  • full_set - array of itself and all children and nested children

These should not be useful, except if you want to write direct SQL:

  • left_col_name - name of the left column passed on the declaration line
  • right_col_name - name of the right column passed on the declaration line
  • parent_col_name - name of the parent column passed on the declaration line



Dissection: move_to_child_of()

vendor/plugins/better_nested_set/lib/better_nested_set.rb

        def move_to_child_of(node)
            self.move_to node, :child
        end

        def move_to(target, position)
          ...

          # compute new left/right for self
          if position == :child
            if target_left < cur_left
              new_left  = target_left + 1
              new_right = target_left + extent
            else
              new_left  = target_left - extent + 1
              new_right = target_left
            end

target is the node that you want to become the new parent of self.

Note that self will become the left-most node of target (new_left = target_left + 1).

I would have preferred that it become the right-most node of target (new_right = target_right - 1), or at least to have the option as to which side it should be inserted on, but currently I don't yet have the confidence to make that change.

Questions

Has there been any effort to get this included in core Rails?

If it's better than the core ActsAsNestedSet, then this should replace it and become the new ActsAsNestedSet?

Has anyone documented the exact differences between ActsAsNestedSet and BetterNestedSet?

And if so, have they tracked any changes...? (Or do I have to do that here...?)

For instance, perhaps Core has added all the features that BetterNestedSet has, rendering BetterNestedSet no longer useful...?



[ActiveRecord / Single table inheritance (category)] and trees

Bugs

http://dev.rubyonrails.org/ticket/7775

  1. 7775 (Derived class types aren't kept when using STI for acts_as_tree) - Rails Trac - Trac


When you create a base class with the acts_as_tree association in it, the class type isn't kept with the children.

class Shape < ActiveRecord::Base
    acts_as_tree
end

class Circle < Shape end

circles = Circle.create!
circles.children.create!

puts circles.class
circles.children.each {|c| puts c.class}

The first will print 'Circle' and the children will print 'Shape'. In my opinion, when a child is created it should look to it's parent to see what it's type should be. Therefore, all nodes in the tree are of the same type.

 



How to migrate a table from ActsAsTree to ActsAsNestedSet

How to migrate a table from ActsAsTree to ActsAsNestedSet edit



Introduction

Let's assume that we have an acts_as_tree model called MenuItem stored in table menu_items and we want to convert it into acts_as_nested_set (I'm actually using BetterNestedSet in this example).

I don't think this migration is possible (easily) without having two models (and thus two tables) -- one with acts_as_tree that we read from and one with acts_as_nested_set that we insert into. I can't imagine trying to transform the data into a nested set by simply doing updates on a single table. It might be possible, but it would require rewriting a lot of the logic for dealing with these two different data structures -- better just to use the code we already have for reading and manipulating them!

Once we've migrated the data to the new table, we can drop the old table and rename the new table to whatever name the old table had.

Details

class MenuItem < ActiveRecord::Base
  acts_as_list :scope => :parent_id
  acts_as_tree :order => :position

  def preorder_traverse(prev_return_value = nil, *args, &visitor)
    cur_return_value = yield self, prev_return_value, *args
    children.each do |item|
      item.preorder_traverse(cur_return_value, *args, &visitor)
    end
  end
  def render_as_text
    preorder_traverse &render_as_text_proc
  end
  def render_as_text_proc
    lambda { |node, *args|
      puts '  '*node.level + "(##{node.id}; #{node.position})" + '. ' +
        "#{node.display_name.to_s.gsub(/./,'*')}" +
        "#{node.name.to_s}"
    }
  end
  def level(i = 0)
    parent ? parent.level(i+1) : i
  end
end

class NestedSetMenuItem < ActiveRecord::Base
  acts_as_nested_set

  def preorder_traverse(prev_return_value = nil, *args, &visitor)
    cur_return_value = yield self, prev_return_value, *args
    children.each do |item|
      item.preorder_traverse(cur_return_value, *args, &visitor)
    end
  end
  def render_as_text
    preorder_traverse &render_as_text_proc
  end
  def render_as_text_proc
    lambda { |node, *args|
      puts '  '*node.level + "(##{node.id}; #{node.lft}-#{node.rgt})" + '. ' +
        "#{node.display_name.to_s.gsub(/./,'*')}" +
        "#{node.name.to_s}"
    }
  end
end

class MigrateTreeToNestedSet < ActiveRecord::Migration
  def self.up
    # Apparently we can't do anything with the table (like add indexes to a column or insert into the table) until we commit it
    # (at least in sqlite3) -- that's why this is in >=2 transactions instead of 1.
    MenuItem.transaction do

      # Create the new table for BetterNestedSet
      create_table "nested_set_menu_items", :force => true do |t|
        t.column :type,   :text                  # For single-table inheritance
        t.column :name,   :text                  # To uniquely identify a menu item or tree
        t.column :display_name,   :text
        t.column :parent_id,      :integer       # For BetterNestedSet
        t.column :lft,            :integer       # For BetterNestedSet
        t.column :rgt,            :integer       # For BetterNestedSet

        t.column :created_at,     :datetime
        t.column :updated_at,     :datetime
      end
    end
    MenuItem.transaction do
      add_index(:nested_set_menu_items, :parent_id)
      add_index(:nested_set_menu_items, :lft)
      add_index(:nested_set_menu_items, :rgt)
      add_index(:nested_set_menu_items, :name)
    end

    MenuItem.transaction do
      # Migrate the data over to the new table

      tree_root     = MenuItem.find_by_name('Root')
      tree_root.preorder_traverse do |tree_item, nested_set_root|
        # Create a new node in the new nested set (neste_set_item) identical to the old node from the tree (tree_item) and
        # make it a child of nested_set_root (unless this is the root node).

        nested_set_item = NestedSetMenuItem.create!(
          :name => tree_item.name,
          :display_name => tree_item.display_name
        )

        if nested_set_root
          nested_set_root.add_child(nested_set_item)
          puts "Added #{nested_set_item.display_name} to #{nested_set_root.display_name}"
        else
          puts "Added #{nested_set_item.display_name} as root"
        end

        nested_set_item
      end

      # Proof that the trees look the same before and after the migration.
      tree_root.render_as_text
      puts '------------------'
      nested_set_root = NestedSetMenuItem.find_by_name('Root')
      nested_set_root.render_as_text

      drop_table 'menu_items'
      rename_table 'nested_set_menu_items', 'menu_items'
    end
  end

  def self.down
    raise IrreversibleMigration
  end
end


== MigrateTreeToNestedSet: migrating ==========================================
-- add_index(:nested_set_menu_items, :parent_id)
   -> 0.0031s
-- add_index(:nested_set_menu_items, :lft)
   -> 0.0014s
-- add_index(:nested_set_menu_items, :rgt)
   -> 0.0014s
-- add_index(:nested_set_menu_items, :name)
   -> 0.0014s
(#1; 1). **************
  (#2; 2). **********************
    (#41; 4). *******************
      (#20; 4). **************************
        (#32; 4). **************
          (#37; 4). ***************************
            (#42; 4). ****************
            (#44; 4). ***********
            (#45; 4). ********
          (#38; 4). ********************
          (#43; 4). ******************
        (#33; 4). *********************
          (#39; 4). **********
          (#40; 4). **********
        (#34; 4). ******************
      (#24; 4). *****
        (#35; 4). ******
        (#36; 4). ******
  (#3; 2). *****
    (#9; 4). ********
      (#18; 4). *************
  (#19; 4). *********
    (#30; 4). ************
      (#46; 4). *******
      (#47; 4). *********
      (#48; 4). ****************
    (#31; 4). *************
      (#49; 4). *********
      (#50; 4). *********
      (#51; 4). ************
      (#52; 4). **********
      (#53; 4). ***********
------------------
(#1; 1-64). **************
  (#2; 2-35). **********************
    (#3; 3-34). *******************
      (#4; 4-27). **************************
        (#5; 5-18). **************
          (#6; 6-13). ***************************
            (#7; 7-8). ****************
            (#8; 9-10). ***********
            (#9; 11-12). ********
          (#10; 14-15). ********************
          (#11; 16-17). ******************
        (#12; 19-24). *********************
          (#13; 20-21). **********
          (#14; 22-23). **********
        (#15; 25-26). ******************
      (#16; 28-33). *****
        (#17; 29-30). ******
        (#18; 31-32). ******
  (#19; 36-41). *****
    (#20; 37-40). ********
      (#21; 38-39). *************
  (#22; 42-63). *********
    (#23; 43-50). ************
      (#24; 44-45). *******
      (#25; 46-47). *********
      (#26; 48-49). ****************
    (#27; 51-62). *************
      (#28; 52-53). *********
      (#29; 54-55). *********
      (#30; 56-57). ************
      (#31; 58-59). **********
      (#32; 60-61). ***********
-- drop_table("menu_items")
   -> 0.0020s
-- rename_table("nested_set_menu_items", "menu_items")
   -> 0.0020s
== MigrateTreeToNestedSet: migrated (2.0795s) =================================
 

After you run the migration, all you have to do is get rid of the old model and move the new, ActsAsTree-powered model into its place...

svn rm app/models/menu_item.rb
svn mv app/models/nested_set_menu_item.rb app/models/menu_item.rb
 



Rails / Sortable trees

Rails / Sortable trees edit


Most of this is a specialization of Rails / Sortable lists.

Scriptaculous's Sortable's tree:true option

Demo: http://script.aculo.us/playground/test/functional/sortable_tree_test.html

Also here: http://svn.tylerrick.com/public/rails/examples/ajax/ajax_examples/app/views/scriptaculous_sortables/tree.rhtml

It's really neat! But... I found it a bit hard to use, actually. Too easy to accidentally move a node up or down a level to a different subtree. Having whole (large) subtrees bumping down dynamically to make room for the element I'm dragging is kind of "neat", but quite distracting and confusing, especially since it does this for every single element as you drag over it, which means it's doing it for a lot of elements...

Conclusion: Don't use the tree:true option. I think generally I would prefer to only allow "easy"/"quick" re-ordering (using the Scriptaculous "Sortable") for nodes at the same level. The user interface to move a node to a different subtree can be accomplished with "plain old" Draggables -- only updating the view after the Ajax request (to assign a new parent) has been processed and the server responds with some RJS to update the view.

A bit laggy perhaps, but it's better than having the [skitterish] feeling of having everything constantly moving/jumping around as you're dragging a node. Ideally, it would be updated on the client-side immediately and then maybe highlighted when the Ajax request completes, but I don't (currently) how easy that would be...

Sur's AjaxTree

Source Code For Ajax Based Drag Drop Navigation Tree in Ruby on Rails (http://ajaxonrails.wordpress.com/2006/08/18/sorce-code-for-ajax-based-drag-drop-navigation-tree-for-rails/) (2006-11-26). Retrieved on 2007-05-11 11:18.


...

http://ajaxonrails.wordpress.com/2006/11/26/ajaxonrailsdragdroptree/ (the source code for the above)

http://svn.tylerrick.com/public/rails/examples/ajaxtree

Comments:

  • You can drag and drop a node to a different parent but not to a different position (well, of course, because it doesn't even keep track of position -- it uses acts_as_tree, which is an unordered tree)
  • It has +/- icons for each node that let you collapse/expand that subtree
  • When you drag and drop, it reloads the entire tree, expanding the subtree that you dropped onto and collapsing all (non-ancestor) trees.
  • There is no indication when you are over a droppable element.
  • Bug: If you drag onto a collapsed node (say 'item4'), it won't just add it as a child of item4, but will actually add it as a child of the last child of item4, which is item4.3 in the demo.
  • By the author's own admission [2], this code has "lagged behind the current trends followed in Rails development" and he was new to Rails when he started it, so it may not use the best conventions...

Sortable tree addition / discussion on Rails-spinoffs list

http://lists.rubyonrails.org/pipermail/rails-spinoffs/2006-March/002890.html [Rails-spinoffs] sortable tree?

http://wrath.rubyonrails.org/pipermail/rails-spinoffs/2006-February/subject.html#2632 The Rails-spinoffs February 2006 Archive by subject

sortable tree fix and enhancement for acts_as_nested_set+better_nested_set plugin

star_full.gif star_full.gif star_empty.gif

http://dev.rubyonrails.org/ticket/7807

  1. 7807 ([PATCH] Sortable tree fixes for #4691 (adding sub-items to empty branches)) - Rails Trac - Trac


1. #4691 fix: you may add any branch under any node and, thus, create new branch. 2. acts_as_nested_set & better_nested_set plugin integration. onChange provides 2 more arguments: movement type and reference object (drop-on-element). Three movements are supported: "left_to", "right_to" and "to_child_of".

http://dev.rubyonrails.org/attachment/ticket/7807/sortable_tree_fix_and_enhancement.2.diff

 


[View-level (category)]

LiveTree

Homepage: http://www.epiphyte.ca/code/live_tree.html
Documentation: RDoc



Description: LiveTree implements a JavaScript/DHTML tree (hierarchical list) widget that can load data asynchronously as-needed (using AJAX). This makes it ideal for cases where the dataset is too large to load to the browser all at once.


License: MIT


Authors: Emanuel Borsboom


[3]

    class FamilyController < ApplicationController
        live_tree :family_tree, :model => :person
        def show_tree
            @root = Person.find(params[:id]);
        end
    end

    <div style="width:300px;height:415px">
        <%= live_tree(:family_tree, {
            :initial_data_root => @root,
            :on_click_item => "alert('You clicked on ' + item.name)",
        }) %>
    </div>

Features

  • Can load data asynchronously as-needed (so only the parts of the tree the user needs are sent to the client).
  • Data can be provided in the HTML page as well, in which case no asynchronous loading is needed, making this widget suitable for standalone client-side use.
  • Ideal for large/deep data sets (such as navigating a file system, or complete table of contents of a book).
  • Intelligently pre-loads parts of the tree that it anticipates the user will want to look at, so delays for the user are minimized.
  • Customizable using CSS, snippets of HTML, event handlers, and many options.
  • Controllable from JavaScript.
  • Supports deep linking - can jump to a part of the tree that has not been loaded yet, and it will search for that item and load its parents.
  • Supports an "active" (highlighted) item.
  • Automatically scrolls the tree so that what the user is interested in is on the screen.
  • Can Use acts_as_tree or acts_as_nested_set model for data from Rails.
  • Easy to integrate into your application.
  • Can be used standalone (client-side only without server support).

Miscellaneous

http://wiki.rubyonrails.org/rails/pages/CategoryTreeUsingActsAsTree

   def display_categories(categories)
     ret = "<ul>" 
     for category in categories
       ret << display_category(category)
     end
     ret << "</ul>" 
   end

  def display_category(category)
    ret = "<li>" 
    ret << link_to h(category.name), :id => category
    ret << display_categories(category.children) if category.children.any?
    ret << "</li>" 
  end
Facts about Rails / TreesRDF feed
Licensed under MIT  +
Description [Oops! No type defined for attribute]
Ads
Personal tools