Monday, February 25, 2008

Rails Forum Tutorial for Beginners(Part 2 of 3)

This is part 2 of my Rails 2.0 Forum tutorial for beginners. You can find part 1 here. Last time, we created our Forum, Topic, and Reply objects. We nested topics inside of forums and adjusted the views/controllers to allow for the nesting. This time, we will take care of replies being nested in a topic.

The nesting in /config/routes.rb has already been taken care of in the first tutorial:
  map.resources :topics, :has_many => :replies


The first thing we are going to do is to create partials for /app/views/replies/new.html.erb and /app/views/replies/edit.html.erb. Create the file /app/views/replies/_reply.html.erb

#/app/views/replies/_reply.html.erb

<% form_for([@topic, @reply]) do |f| %>
<p>
<b>Subject</b><br />
<%= f.text_field :subject %>
</p>

<p>
<b>Body</b><br />
<%= f.text_area :body %>
</p>

<p>
<%= f.submit button_name %>
</p>
<% end %>


And now change edit and new to:


#/app/views/replies/edit.html.erb

<h1>Editing reply</h1>

<%= error_messages_for :reply %>

<%= render :partial => @reply, :locals => { :button_name => "Submit" } %>

<%= link_to 'Show', [@topic, @reply] %> |
<%= link_to 'Back', replies_path %>



#/app/views/replies/new.html.erb

<h1>New reply</h1>

<%= error_messages_for :reply %>

<%= render :partial => @reply, :locals => { :button_name => "Reply" } %>

<%= link_to 'Back', replies_path %>


Same stuff we did last time...
Now lets go to /app/controllers/replies_controller.rb
Add this at the top, to make sure a topic gets loaded everytime the replies controller is used:

before_filter :load_topic

def load_topic
@topic = Topic.find(params[:topic_id])
end

inside the new function change the line:

@reply = Reply.new

to this:

@reply = @topic.replies.build

Change the edit function:

#old edit function

def edit
@reply = Reply.find(params[:id])
end


#new edit function

def edit
@reply = @topic.replies.find(params[:id])
end

Change the create function:

#old create

def create
@reply = Reply.new(params[:reply])

respond_to do |format|
if @reply.save
flash[:notice] = 'Reply was successfully created.'
format.html { redirect_to(@reply) }
format.xml { render :xml => @reply, :status => :created, :location => @reply }
else
format.html { render :action => "new" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end


#new create

def create
@reply = @topic.replies.build(params[:reply])

respond_to do |format|
if @reply.save
flash[:notice] = 'Reply was successfully created.'
format.html { redirect_to([Forum.find(@topic.forum_id), @topic]) }
format.xml { render :xml => @reply, :status => :created, :location => @reply }
else
format.html { render :action => "new" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end



Notice the change in redirect_to...after a reply to a topic is submitted, I want to send the user back to the forum the topic was in, to get the forum, I had to use the find function and pass it the forum_id of the topic and the topic.


In the update and destroy functions:

#old update and destroy

def update
@reply = Reply.find(params[:id])

respond_to do |format|
if @reply.update_attributes(params[:reply])
flash[:notice] = 'Reply was successfully updated.'
format.html { redirect_to(@reply) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end

def destroy
@reply = Reply.find(params[:id])
@reply.destroy

respond_to do |format|
format.html { redirect_to(replies_url) }
format.xml { head :ok }
end
end



#new update and destroy

def update
@reply = @topic.replies.find(params[:id])

respond_to do |format|
if @reply.update_attributes(params[:reply])
flash[:notice] = 'Reply was successfully updated.'
format.html { redirect_to(@topic) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @reply.errors, :status => :unprocessable_entity }
end
end
end


def destroy
@reply = @topic.replies.find(params[:id])
@reply.destroy

respond_to do |format|
format.html { redirect_to(@topic) }
format.xml { head :ok }
end
end


Now that we have all of that done...lets head over to /app/views/topics/show.html.erb so we can display our replies and the form for a new reply.


In addition to showing replies and a form for a new one, I changed the formatting a little, so here are the contents on show.html.erb

<p>
<h3>Forum: - <%=h @forum.name %></h3>
</p>

<p>
<b><%=h @topic.subject %></b>
</p>

<p>
<%=h @topic.body %>
</p>

<% unless @topic.replies.empty? %>

<% @topic.replies.each do |reply| %>
<p>
<b><%= reply.subject %></b>
</p>
<p>
<%=h reply.body %>
</p>

<% end %>
<% end %>

<h2>Reply</h2>
<%= render :partial => @reply = Reply.new, :locals => { :button_name => 'Reply' } %>

<%= link_to 'Edit', edit_forum_topic_path(@forum, @topic) %> |
<%= link_to 'Back', topics_path %>



Since this is the same stuff we were doing in the first tutorial, I won't bother explaining any of it...you should be able to tell what is going on. Feel free to leave a comment if you do have any questions.


Now, fire up the web server and head to localhost:3000/forums/1/topics/1 (assuming you have a topic already created).






And create a couple of replies:





And thats it! (10 points go to anyone who catches the video game reference in reply #2 in the screenshot).

In this tutorial, we nested replies inside of a topic, and set the views/controllers accordingly. Next time, we will take a look at authentication using the RESTful authentication plug-in and acts_as_state_machine plugin.

Feel free to leave comments, and link me from your site. See you next time!

16 comments:

  1. Thank you for this great tutorial.

    I'am looking forward to the last part.

    Marek

    ReplyDelete
  2. Flamestrike! Ultima online references in the screenshot.

    ReplyDelete
  3. Thank you for making RoR seem less like Harry Potter & more like Transformers. Currently I am having problems with line 29 in topics/show.html.erb should there be [square brackets] around the params in edit_topic_path(@forum, @topic)?

    Regards,
    Bill

    ReplyDelete
  4. (ahhhhhh, just answered my own question)

    ReplyDelete
  5. I just found your tutorial and it's excellent. Also the video game reference would the the words of power from Ultima Online

    ReplyDelete
  6. lol "kal vas flam" !

    Nice to see some UO fans out there!

    Great tute, I'd love to see some comments on Ruby syntax, I can't find a tutorial where learning Rails has some dashes of Ruby thrown in...

    ReplyDelete
  7. Part 2 is nice reinforcement for what was learned in part 1, and some of the differences provided insight into the workings of MCV.

    The only thing I did a bit differently was to add

    validates_presence_of :subject, :body

    to the app\models reply.rb and topic.rb files.

    ReplyDelete
  8. you blog is very helpfull!thanks.

    the topics/show.html.erb
    last line is better replace by this:
    = link_to 'Back', forum_path

    ReplyDelete
  9. When I click the "Reply" button,
    I got this error:

    ArgumentError in RepliesController#create
    wrong number of arguments (2 for 1)

    RAILS_ROOT: D:/railsProjects/myforum
    Application Trace | Framework Trace | Full Trace
    app/controllers/replies_controller.rb:54:in `[]'
    app/controllers/replies_controller.rb:54:in `create'
    app/controllers/replies_controller.rb:51:in `create'

    Pls give me some suggestion.
    Thanks.

    ReplyDelete
  10. When click a topic,then get the following error:
    NoMethodError in Topics#show

    Showing topics/show.html.erb where line #15 raised:
    You have a nil object when you didn't expect it!
    The error occurred while evaluating nil.subject

    Extracted source (around line #15):
    12:
    13: unless @topic.replies.empty?
    14:
    15: @reply.subject
    16:
    18: @reply.body


    RAILS_ROOT: D:/railsProjects/myforum

    What's the problem here?
    Could anyone help me?
    Thanks.

    ReplyDelete
  11. It works for me now.

    Find the problem.

    ReplyDelete
  12. I'm getting a problem after a reply:


    undefined method `replies' for # < Topic:0xb7458348 >

    RAILS_ROOT: /home/friedman/public_html/studentsfirst
    Application Trace | Framework Trace | Full Trace

    /usr/lib/ruby/gems/1.8/gems/activerecord-2.1.2/lib/active_record/attribute_methods.rb:256:in `method_missing'
    app/controllers/replies_controller.rb:51:in `create'



    Could you be of any help??

    Thanks,
    Jacob

    ReplyDelete
  13. Never mind! Fixed it. Was a typo in a model. Figures...

    Again, thank you for your time to make such a great tutorial. Your efforts are nothing less than noble.

    ReplyDelete
  14. Goodness, there's a great deal of useful info in this post!

    ReplyDelete