Implement acts_as_threaded without a plugin 30

Posted by Bob Silva Fri, 31 Mar 2006 20:40:00 GMT

It's been awhile since I've posted so I thought I'd toss this out there. If you've used my acts_as_threaded plugin, you know its an off-shoot from the acts_as_nested set inside of AR itself. I've managed to create the same functionality without the use of a plugin using the native acts_as_nested_set feature of AR. The relevant code is below. The threading functionality has been moved to the model now, add the following code to your model and you are set.
  acts_as_nested_set :scope => :root
    
  def before_create
    # Update the child object with its parents attrs
    unless self[:parent_id].to_i.zero?
      self[:depth] = parent[:depth].to_i + 1
      self[:root_id] = parent[:root_id].to_i
    end
  end
  
  def after_create
    # Update the parent root_id with its id
    if self[:parent_id].to_i.zero?
      self[:root_id] = self[:id]
      self.save
    else
      parent.add_child self
    end
  end
  
  def parent
    @parent ||= self.class.find(self[:parent_id])
  end

Your database schema will need to have the following definition:
create_table "my_table_name", :force => true do |t|
  t.column "root_id", :integer
  t.column "parent_id", :integer
  t.column "lft", :integer
  t.column "rgt", :integer
  t.column "depth", :integer
end
The trick from this point, is that whenever you create a new thread, if it has a parent_id, then it will automatically be added as a child to that parent record. Otherwise, it will be set as a root thread. This version no longer requires that the fields have a default value of 0 relying on the fact that 'NilClass.to_i == 0'.

Hope you enjoy this, it's come in very handy for modeling structured content in some of my apps (like categories and multi-level organizations).
Comments

Leave a response

  1. Enrico Mingardo 3 months later:
    I've made a little change to make it works for me....
      def before_create
        unless self[:parent_id].to_i.zero?
          self[:depth] = parent[:depth].to_i + 1
          self[:root_id] = parent[:root_id].to_i
        end
      end
    
    bye em
  2. josh 3 months later:
    Nice. But what is the depth field required for? The nested set code doesn't use it at all.
  3. Ben 3 months later:
    I believe that the depth field is for when you actually display the posts.

    For each post, you put it in a span/div and do style="margin: 0 0 0 "

    That way the children are indented based on how deep they are (in this case, 10 pixels more indented per level)
  4. Bob Silva 3 months later:
    Hi Josh, Ben nailed it. The depth column is the main difference that allows you to pull all the records at once and display them in proper relationship. WIthout the depth column, nested_set would have to make recursive queries to pull the records AND represent the relationship between them.
  5. Ben 3 months later:
    Why do you make the display of the text a method helper, instead of a partial?
  6. Ben 3 months later:
    errr *helper method*
  7. Ben 3 months later:
    Another thing - I'm having problems with the paginate. When I click next, it reloads the page, but doesn't iterate to the next page. Any ideas?
  8. kublaikhan55@hotmail.com 3 months later:
    Hi, Could someone post the code for the module PostHelper of the demo? Some of the lines in the screencast were truncated, and, sadly, I don't have enough in me to reconstruct the missing parts. Thanks!
  9. Ben 3 months later:
    He posted it in a code block if you scroll down just a tad here http://www.railtie.net/articles/2006/02/05/rails-acts_as_threaded-plugin
  10. Bob Silva 3 months later:
    Hi Ben, For the helper vs. partial, I simply didn't know what a partial was when I did the screen cast. I was only into Rails for a few days (hadn't read the book yet) but got really interest when I saw acts_as_nested_set. I just redid it as a parital with a collection and it is definately a better solution. As for the pagination, it works fine for me, not sure about that.
  11. kublaikhan55@hotmail.com 3 months later:
    Ben, How could I have not seen that code!? Duh. Thanks much! gk
  12. Wouf 4 months later:
    First day with Ruby and Rails... It's really impressive, all my questions have a 'minute build' reply ;) Nice code, Thanks much !
  13. Mason 4 months later:
    Pretty impressive stuff you've done here. Quick question - I'm trying acts_as_threaded out on some existing data (locations where office is root and satellite-office is child). When I call Location.add_child() on an office, it sets the lft, rgt, parent_id, and depth properly, but root_id of both parent and child stay NULL. I see where the root_id should be set in after_create, but since the parent record already exists this never happens - it would need after_save or after_update, wouldn't it? Thanks!
  14. Mason 4 months later:
    I meant to post this on the page about the plugin.... Whoops. I've discovered why you can't do after_save containing a save - recursive saves that cause mysql to give up. I'll keep looking for some other way around it.... Thanks!
  15. Bruce 4 months later:
    Hi All, Works great, but I had to add a few things. I modified my forum migration so the depth is 0 by default for the top level forums. Also made a partial to display the forums. I had to add "px;" to the style so the indentation would happen. Here's the code for my _forum.rhtml partial: 'show', :id = forum.id}, {'style' = 'color: #00f'}) + " " + content_tag('span', " by #{h forum.user.login} ยท #{forum.created_at.strftime('%b, %d %Y - %I:%M %p')}", 'style' = 'font:10px tahoma;color:#666;')), 'style' = "margin:5px 0px;padding-left:#{forum.depth*20}px;") %
  16. philip tsai 5 months later:
    Hello Bob, Is it just me or the code shown above may not work now? I created a new AR model. I pasted in the code shown above. Creating the first root post (let's call it m1) works fine but when I am creating the child post, m2, as such: m2 = Post.create(:body='this is a reply', :parent_id = m1.id) -- things look OK until I query m1.all_children which return [] and m1.children_count() return -1. Looking into the DB table, I see that the lft field for m2 stays at 0.... What could be going wrong here?...
  17. philip tsai 5 months later:
    Hello Bob, Is it just me or the code shown above may not work now? I created a new AR model. I pasted in the code shown above. Creating the first root post (let's call it m1) works fine but when I am creating the child post, m2, as such: m2 = Post.create(:body='this is a reply', :parent_id = m1.id) -- things look OK until I query m1.all_children which return [] and m1.children_count() return -1. Looking into the DB table, I see that the lft field for m2 stays at 0.... What could be going wrong here?...
  18. philip tsai 5 months later:
    the other thing that I find strange is that -- when a post is newly created without any children, that post.root? should return true, right? But the code for the method, in the plug-in version as well, is the same as nested_set's -- which means that it is only a root when lft=1 and rgt lft... Wouldn't the semantics for at least acts_as_threaded be that a newly created post is a root by itself?
  19. philip tsai 5 months later:
    So the problem, regarding to my first post above, was that following the screencast, I had all five column with :null = false constraints on. Looking into the acts_as_nested_set code helped me realize this. Taking those constraints out now works as the plug-in version. Honestly, I like more constraints if appropriate, so the plugin version works better; however, I also prefer to augment on existing plugin -- more portable.... As for the root post... I guess I will just overwrite the method to fit the semantics that I deem appropriate.
  20. Luis Carrasco 5 months later:
    Hi, I have been having trouble trying to show the nested tree as a list, i mean using UL and LI and stuff like that, to later use it whith http://www.dhtmlgoodies.com/index.html?whichScript=drag-drop-folder-tree Any one can help me? Thanks in advance!
  21. Luis Carrasco 5 months later:
    Hi, I have been having trouble trying to show the nested tree as a list, i mean using UL and LI and stuff like that, to later use it whith http://www.dhtmlgoodies.com/index.html?whichScript=drag-drop-folder-tree Any one can help me? Thanks in advance!
  22. Luis Carrasco 5 months later:
    Hi, I have been having trouble trying to show the nested tree as a list, i mean using UL and LI and stuff like that, to later use it whith http://www.dhtmlgoodies.com/index.html?whichScript=drag-drop-folder-tree Any one can help me? Thanks in advance!
  23. http://www.railsroad.com 6 months later:
    I've been trying to figure out how to get this to work. I don't understand how the rgt and lft attributes are generated. I looked at the code for acts_as_nested_list and did not see where that data would be generated either.
  24. Bryan 6 months later:
    I've been trying to figure out how to get this to work. I don't understand how the rgt and lft attributes are generated. I looked at the code for acts_as_nested_list and did not see where that data would be generated either.
  25. Bryan 6 months later:
    I've been trying to figure out how to get this to work. I don't understand how the rgt and lft attributes are generated. I looked at the code for acts_as_nested_list and did not see where that data would be generated either.
  26. Praxedis 6 months later:
    Here's my example without the use of the plugin: NeoAztlan forum: http://forum.neoaztlan.com Bryan, if you're having issues, start with the acts as threaded plugin tutorial first. It describes the model and everything.
  27. Farooq Ali 8 months later:
    I found this useful to get all ancestors in a single query:
    def ancestors
      if root? then []
      else
        Topic.find(:all, :conditions => "id = #{root_id} or " +
                         "(root_id = #{root_id} and depth < #{depth} " +
                         "and #{left_col_name} < #{self[left_col_name]} " +
                         "and #{right_col_name} > #{self[right_col_name]})",
                         :order => 'depth')
      end
    end
    
  28. Jonas 10 months later:
    Thanks Bob for spreading the information to noobs like me! Is there any way to accomplish a sort of "acts_as_list" with a nested set, so that you can place order children, roots and whatever in certain positions? Thanks again..!
  29. Michael Carney 12 months later:
    I ran into a similar problem to Philip Tsai above where I was getting the first response to an item having a lft child with a value of zero. I followed his advice, but then ran into a different problem. If you have a post with no children and call children_count, you end up with a problem because of the NULL values for lft and rgt. I ended up with a different solution which I think feels better. I modified the original code from above as follows: [code] def after_create # Update the parent root_id with its id if self[:parent_id].to_i.zero? self[:root_id] = self[:id] self[:lft] = 1 self[:rgt] = 2 self[:depth] = 0 self.save else parent.add_child self end end [/code] The values of 1 or 2 are the default values that are assumed by nested_set if lft or rgt is NULL, but by setting them here all of the code works properly even if there are no children. For instance, calling all_children returns an empty array rather than barfing because of a poorly formed SQL query (with things like "(rgt )" because the value of rgt is NULL). This seems pretty bullet proof. Does anybody see any problems with it? Thanks.
  30. Michael Carney 12 months later:
    Sorry about the formattingg above. I couldn't figure out how to get paragraphs and code in. But the idea is to set lft=1, rgt=2, and depth=3 in the after_create callback at the same timee that you set root_id=id. Any thoughts are appreciated. Thanks.
Comments