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:

Anonymous said...

Thank you for this great tutorial.

I'am looking forward to the last part.

Marek

Stephane Chouinard said...

Flamestrike! Ultima online references in the screenshot.

rledge21 said...

chouinard += 10

Bill said...

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

Bill said...

(ahhhhhh, just answered my own question)

Mitchell Blankenship said...

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

Wintermute said...

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...

Anonymous said...

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.

Anonymous said...

you blog is very helpfull!thanks.

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

Unknown said...

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.

Unknown said...

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.

Unknown said...

It works for me now.

Find the problem.

Unknown said...

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

Unknown said...

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.

Diva said...

nice tutor, keep it up..

muebles rioja said...

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