Recipe 3.16. Modeling a Threaded Forum with acts_as_nested_set


Problem

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.

Solution

Create a posts table with following Active Record migration. Make sure to insert an initial parent topic into the posts table, as this migration does:

db/migrate/001_create_posts.rb:

class CreatePosts < ActiveRecord::Migration   def self.up     create_table :posts do |t|       t.column :parent_id, :integer       t.column :lft, :integer       t.column :rgt, :integer       t.column :subject, :string       t.column :body, :text     end          Post.create :subject => "What's on your mind?"   end   def self.down     drop_table :posts   end end

Then specify that the Post model is to contain data organized as a nested set by calling acts_as_nested_set in the Post class definition.

app/models/post.rb:

class Post < ActiveRecord::Base   acts_as_nested_set end

Next, set up data structures and logic for the forum's view and its basic post operations:

app/controllers/posts_controller.rb:

class PostsController < ApplicationController   def index     list         render :action => 'list'   end   def list     @posts = Post.find(:all,:order=>"lft")   end   def view     @post = Post.find(params[:post])     @parent = Post.find(@post.parent_id)   end   def new     parent_id = params[:parent] || 1     @parent = Post.find(parent_id)     @post = Post.new   end   def reply      parent = Post.find(params["parent"])     @post = Post.create(params[:post])     parent.add_child(@post)     if @post.save       flash[:notice] = 'Post was successfully created.'     else           flash[:notice] = 'Oops, there was a problem!'     end     redirect_to :action => 'list'   end end

The new.rhtml template sets up a form for creating new posts:

app/views/posts/new.rhtml:

<h1>New post</h1> <p>In response to:; <b><%= @parent.subject %></b></p> <% form_tag :action => 'reply', :parent => @parent.id do %>   <%= error_messages_for 'post' %>   <p><label for="post_subject">Subject:</label>;   <%= text_field 'post', 'subject', :size => 40 %></p>   <p><label for="post_body">Body:</label>;   <%= text_area 'post', 'body', :rows => 4 %></p>   <%= submit_tag "Reply" %> <% end %> <%= link_to 'Back', :action => 'list' %>

Define a Posts helper method named get_indentation that determines the indentation level of each post. This helper is used in the forum's thread view.

app/helpers/posts_helper.rb:

module PostsHelper   def get_indentation(post, n=0)     $n = n       if post.send(post.parent_column) == nil       return $n     else           parent = Post.find(post.send(post.parent_column))        get_indentation(parent, $n += 1)     end   end end

Now, display the threaded form in the list.rhtml view with:

app/views/posts/list.rhtml:

<h1>Threaded Forum</h1> <% for post in @posts %>   <% get_indentation(post).times do %>_&nbsp;<% end %>   <%= post.subject %>   <i>[     <% unless post.send(post.parent_column) == nil %>       <%= link_to "view", :action => "view", :post => post.id %> |      <% end %>     <%= link_to "reply", :action => "new", :parent => post.id %>   ]</i> <% end %>

Finally, add a view.rhtml template for showing the details of a single post:

app/views/posts/view.rhtml:

<h1>View Post</h1> <p>In response to: <b><%= @parent.subject %></b></p> <p><strong>Subject:</strong> <%= @post.subject %></p> <p><strong>Body: </strong> <%= @post.body %></p> <%= link_to 'Back', :action => 'list' %>

Discussion

acts_as_nested_set is a Rails implementation of a nested set model of trees in SQL. acts_as_nested_set is similar to acts_as_tree, except that the underlying data model stores more information about the positions of nodes in relation to each other. This extra information means that the view can display the entire threaded forum with a single query. Unfortunately, this convenience comes at a cost when it's time to write changes to the structure: when a node is added or deleted, every row in the table has to be updated.

An interesting part of the solution is the use of the helper method get_indentation. This is a recursive function that walks up the tree to count the number of parents for each node in the forum. The number of ancestors that a node has determines the amount of indentation.

Two links are placed next to each post. You can view the post, which displays its body, or you can reply to the post. Replying to a post adds a new post to the set of posts directly underneath that parent post.

In the list view and the get_indentation helper, the parent_column method is called on the post object. That call returns parent_id by default, and in turn uses the send method to call the parent_id method of the post object.

post.send(post.parent_column)

This notation allows you to change the name of the default column used for parent records. You specify a parent column of topic_id in the model class definition by passing the :parent_column option to the acts_as_nested_set method:

class Post < ActiveRecord::Base   acts_as_nested_set :parent_column => "topic_id"  end

Figure 3-9 shows the list view of the solution's forum.

Figure 3-9. A threaded forum made using acts_as_nested_set


See Also

  • Chapter 28, "Trees and Hierarchies in SQL," from Joe Celko's SQL for Smarties: Advanced SQL Programming, Third Edition (Morgan Kaufmann)




Rails Cookbook
Rails Cookbook (Cookbooks (OReilly))
ISBN: 0596527314
EAN: 2147483647
Year: 2007
Pages: 250
Authors: Rob Orsini

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net