This is my first tutorial, so bear with me. I am writing it as if trying to teach someone brand new to rails what is going on. We are going to be creating a forum site in Ruby on Rails(2.0). I plan on this to be a several-part series, so keep coming back and checking for updates.
So here we go!
rails myforum -d mysql
This will create the project folder 'myforum' and force the database used to mysql(the newest version of rails default to sqlite3). All kinds of files and folders are created, but don't let them overwhelm you. Rails is an application framework, so by creating all this stuff it will force you to keep everything organized according to Rails' conventions.
Now, the general layout I decided on for the forums is this: The site will have multiple forums, each forums is going to have multiple topics, and each topic will have multiple replies. To get this going, lets try this:
cd myforum
ruby script/generate scaffold Forum name:string description:text
More files and directories are created, setting up migration files for the database, a model, a controller, and views for the Forum class. Lets analyze the command we just gave:
ruby script/generate - on a mac or on linux, it would be script/generate(I'm on windows). You will be using script/generate for a lot of things in rails, so get used to typing that one.
scaffold Forum - This tells rails that you want to generate a scaffold for the class 'Forum
name:string description:text - in the forums table that will be created, it will have 2 columns: name, and description. the :string and :text are the type of data, with string being a small 1 liner used for naming, and text being something that can be much longer. Moving on....
rake db:create
rake db:migrate
rake db:create will create your mySQL database for you (called myforum_development). rake db:migrate will create the table for the forum class.
Now lets fire up the server and see what we've got!
ruby script/server
And point your web browser(and you better not be using IE!) to http://localhost:3000
So the server is working fine, now go to http://localhost:3000/forums
If you click on New forum, it will give you a form with fields for Name and Description.
And then click back:
Thats right! With only 4 lines of code, rails was able to generate all of this! The scaffolding will create everything you need to Create, Read, Update, and Delete(CRUD).
Moving on, lets set things up so that rails will show the list of forums page by just going to http://localhost:3000, and get rid of that rails info page. Delete the file public/index.html, and then in config/routes.rb do this:
#In config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :forums
map.root :controller => 'forums', :action => 'index' # <-------add this line
..........
..........
end
So this is our first editing of a rails file. routes.rb is very important to the whole rails applications. They take a request for a URL and turn it into a request from a controller and an action. In this case, the line we added will take a request for the root(http://localhost:3000) and give us back the index action of the forums controller(which will list all the forums). Save, and again, point the browser to http://localhost:3000:
Great! Now, lets go ahead and generate scaffolding for our other 2 classes: Topics and Replies:
ruby script/generate scaffold Topic forum:references user:references subject:string body:text
ruby script/generate scaffold Reply topic:references user:references subject:string body:text
rake db:migrate
We have something new here. forum:references means that a topic will contain a foreign key to the forum it belongs to. It will also contain a foreign key for the user the posts it. Additionally, a reply will belong to a certain topic. And again, rake db:migrate will create the additional tables in the database. Don't worry about the User class yet, that part will be in a later tutorial when we add authentication.
If you go to http://localhost:3000/topics and http://localhost:3000/topics, you will see similar pages to the forum page that was created earlier.
Now that we have our scaffolding up, its time to take a look at some of the code that was generated and put it where we want it. First, lets go to /config/routes.rb and do a few things:
BEFORE:
#In config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :replies
map.resources :topics
map.resources :forums
map.root :controller => 'forums', :action => 'index'
...............
...............
end
AFTER:
#In config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :forums, :has_many => :topics
map.resources :topics, :has_many => :replies
map.resources :replies
map.root :controller => 'forums', :action => 'index'
...............
................
end
This sets up routing so that to view a specific post, you would go to a URL like: /forums/1/posts/3, and to see a reply /posts/1/replies/3.
Moving on, lets edit the files in /app/models/
#forum.rb
class Forum < ActiveRecord::Base
has_many :topics
has_many :replies, :through => :topics
end
------------------------------
#topic.rb
class Topic < ActiveRecord::Base
belongs_to :forum
belongs_to :user
has_many :replies
end
-------------------------------
#reply.rb
class Reply < ActiveRecord::Base
belongs_to :topic
belongs_to :user
end
All of that should make sense to you, just fleshing out some of the stuff we've already talked about.
Next, lets check out /app/controllers/topics_controller.rb. Add these lines to it:
before_filter :load_forum
def load_forum
@forum = Forum.find(params[:forum_id])
end
before_filter :load_forum will cause the load_forum method to be called anytime the topics controller is accessed.
Now, in the index function, change:
@topics = Topic.find(:all)
to
@topics = @forum.topics
Now change every instance of:
@topic = Topic.find(params[:id])
to
@topic = @forum.topics.find(params[:id])
and in the new function:
@topic = Topic.new
to
@topic = @forum.topics.build
Change the create function from:
def create
@topic = Topic.new(params[:topic])
respond_to do |format|
if @topic.save
flash[:notice] = 'Topic was successfully created.'
format.html { redirect_to(@topic) }
format.xml { render :xml => @topic, :status => :created, :location => @topic }
else
format.html { render :action => "new" }
format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
end
end
end
to
def create
@topic = @forum.topics.build(params[:topic])
respond_to do |format|
if @topic.save
flash[:notice] = 'Topic was successfully created.'
format.html { redirect_to(@forum) }
format.xml { render :xml => @topic, :status => :created, :location => @topic }
else
format.html { render :action => "new" }
format.xml { render :xml => @topic.errors, :status => :unprocessable_entity }
end
end
end
In update, change the line:
format.html { redirect_to(@topic) }
to
format.html { redirect_to(@forum) }
and finally, in destroy, change the line:
format.html { redirect_to(topics_url) }
to
format.html { redirect_to(forum_topics_url(@forum)) }
Alright, lets take a look at all that we just did. With the code that was generated by the scaffold, Rails did not take into account the fact that we need to retrieve our topic classes by a foreign key(forum_id). The majority of the changes were in setting that up correctly. The other changes were in the redirect_to function calls. Whenever I create, update, or destroy a topic, I want it to redirect back to the forum I was on instead of going back to the topic page automatically assigned.
Now, when we go to look at a specific forum, we want it to list out not just the forum title and description, but all the topics associated with that forum. Open up /app/views/forums/show.html.erb
<p>
<b>Name:</b>
<%=h @forum.name %>;
</p>
<p>
<b>Description:</b>
<%=h @forum.description %>
</p>
<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>
Since this is our first look at a view, let go over it a bit.
The <% tags work kind of like <?php tags. They switch out of normal HTML markup to ruby code. Inside these tags are where the magic happens. So a tag with just <% will not display anything, they are useful for doing things like loops. A tag with <%= will output the result into the HTML markup. So the line <%=h @forum.name %> will display the name of the forum. The lowercase 'h' is there escape HTML characters in output.
Change the file to this:
<p>
<b>Forum:</b>
<%=h @forum.name %> - <%=h @forum.description %>
</p>
<% unless @forum.topics.empty? %>
<h2>Topics</h2>
<% @forum.topics.each do |topic| %>
<b><%= link_to topic.subject, [@forum, topic] %></b><br />
<% end %>
<% end %>
<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>
Now one more thing; I want to have a 'Create New Topic' form at the bottom of the page. First, lets take a look at /app/views/topics/new.html.erb and /app/views/topics/edit.html.erb. These 2 files are nearly identical. This is a good time to use the DRY(Don't Repeat Yourself) that Rails is big on. Lets create a new file called _topic.html.erb in the /app/views/topics/ folder and put the following code into it:
<% form_for([@forum, @topic]) 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 then change edit.html.erb and new.html.erb to look like this:
#edit.html.erb
<h1>Editing topic</h1>
<%= error_messages_for :topic %>
<%= render :partial => @topic, :locals => { :button_name => "Submit" } %>
<%= link_to 'Show', [@forum, @topic] %> |
<%= link_to 'Back', topics_path %>
----------------------------
#new.html.erb
<h1>New topic</h1>
<%= error_messages_for :topic %>
<%= render :partial => @topic, :locals => { :button_name => "Create" } %>
<%= link_to 'Back', topics_path %>
That cleaned up a good bit of code. Since the only real difference in the 2 forms for those pages was the button name, we created what is called a partial in the file _topic.html.erb, and then rendered the partial in new.html.erb and edit.html.erb, passing in the value of the button name. And now, we can also re-use the partial in /app/views/forums/show.html.erb
Change the file to look like this:
<p>
<b>Forum:</b>
<%=h @forum.name %> - <%=h @forum.description %>
</p>
<% unless @forum.topics.empty? %>
<h2>Topics</h2>
<% @forum.topics.each do |topic| %>
<b><%= link_to topic.subject, [@forum, topic] %></b><br />
<% end %>
<% end %>
<h2>New Post</h2>
<%= render :partial => @topic = Topic.new, :locals => { :button_name => 'Create' } %>
<%= link_to 'Edit', edit_forum_path(@forum) %> |
<%= link_to 'Back', forums_path %>
One more thing, open up /app/views/topics/show.html.erb and remove this part:
<p>
<b>User:</b>
<%=h @topic.user %>
</p>
Since we haven't implemented the authentication yet.
And change the line:
<%=h @topic.forum %>
to
<%=h @forum.name %>
And then change:
<%= link_to 'Back', topics_path %>
to
<%= link_to 'Back', forum_path(@forum) %>
Save everything, fire up the server, and head back to http://localhost:3000
ruby script/server
Click Show
And there we go! You can add a new post, and then click on it to show it.
So far in the tutorial, we have generated scaffolding for 3 classes(forum, topic, reply), set up the nested routing for topics to be inside forums, and replies to be inside topics, and modified the controllers and view for forum and topic to work like we need it to.
I've decided this will be a 3-part tutorial. The next part will be to get the Replies working(it will be very similar to what we did with nesting topics inside forums) by modifying the views for topics, and the views and controllers for replies. The third part will be to set up Authentication and Admin priveleges.
Let me know what you think, or if you have any questions. And tell your friends.
See you next time!