Advertisement
Other

Building a Forum From Scratch with Ruby on Rails

by

Today, we will be building a simple forum using Ruby on Rails, and we will be working up from the basics covering things like authentication and more advanced database techniques.

Final Product

Ruby on Rails

If you're unfamiliar with Ruby on Rails, it is a web application framework built using the Ruby programming language. There have been some good tutorials published here on net.tuts+ on getting starting with Rails, and for a quick review you can see my previous article.

Step 1 - Create the Rails Application

To create our application, we use the rails command, followed by the name of our project, and that happens to be forum in this case. The terminal command below is what I used. Also, make sure you have changed into the proper directory in your terminal.

rails forum

After running the command you will see a long line of text with all the files that have just been created. Now you will want to change into the directory we just created.

Step 2 - Installing Gems

You are probably wondering what a Gem is. A Gem is a self-contained package for distributing Ruby programs and libraries. They come in handy very often, they make your life easier. We will be using a few gems for this project, listed below. Also, make sure that you have the latest version of RubyGems and your Gems by running:

gem update --system

(with sudo if needed)

gem update

(with sudo if needed)

  • nifty-generators: Ryan Bates's amazing nifty-generators will be used for the nifty-authentication and nifty-scaffold generators
  • database: Make sure you have any gem you need to interface with the database you will use, I am going to use SQLite

Step 3 - Setting up the Authentication

Now that we have all the gems we need installed, run the following in your terminal: (make sure you are in the directory rails created)

script/generate nifty_authentication

You should see text fly by like above. Now go ahead and open up the folder in your favorite IDE. We are first going to edit the config/database.yml file. The file should be set up to use whatever database system you want, and in this case I will be using SQLite.

Making sure everything is setup properly, and then run the following command to migrate changes to our database:

rake db:migrate

Step 4 - The Application Structure

No, this part is not going to actually deal with the application layout in terms of overall design, we will work on that shortly. This section will actually deal with how the application will be built. One of the major upsides with Ruby on Rails is the plethora of generators available. The only problem that some people miss is that you need to plan you application to make these generators really go the extra-mile.

Below is how the models in the database will be organized and related is a rough manner:

  • Forum: A forum is the top-most level. Basically a group of topics that relate to one another.
    • Topic: a topic, also called a thread concerns a certain topic.
      • Post: a post belongs to a topic, and is a message by a certain user.

So we are going to use nifty-scaffold to generate three models, one for each of the parts listed above. So getting started, we are going to use the scaffold to create our forum. To do this, run the following command:

script/generate nifty_scaffold forum name:string description:text

Next we will scaffold for the topic:

script/generate nifty_scaffold topic name:string last_poster_id:integer last_post_at:datetime

Now we will scaffold the post:

script/generate nifty_scaffold post content:text

Then migrate the database:

rake db:migrate

Step 5 - Creating the Layout

Now, we are actually going to work on the the first part of the overall look of the site. Due to the fact we are using nifty_scaffold, we are also going to use nifty_layout to generate some of the basic layout, that we will change. To use nifty_layout, run:

script/generate nifty_layout

Now this was a real quick step just to get this working for now. We will come back to this later.

Step 6 - Setting up the Associations

So we are going to create associations in our models. Associations are ways to link models. Let's first open up our app/models/forum.rb and you should see:

# app/models/forum.rb
class Forum < ActiveRecord::Base
end

Now we are going to add some code and it should look like the following, and I will explain it in a minute.

# app/models/forum.rb
class Forum < ActiveRecord::Base
  has_many :topics, :dependent => :destroy
end

The code has_many means exactly what it sounds like; it 'has many' and then we refer to our model Topic. You may wonder why it works when you add an 's', and that is the ActiveRecord convention. The :dependent => :destroy means that when the forum is deleted, all of it's topics will be too. Now we are going to open up our app/models/topic.rb and edit it to the following:

# app/models/topic.rb
class Topic < ActiveRecord::Base
  belongs_to :forum
  has_many :posts, :dependent => :destroy
end

Again, this is plain text. It says a Topic belongs to a Forum, and has many posts. We are also using :dependent => :destroy here to delete all of it's children posts. Now open up app/models/post.rb and edit it to the following:

# app/models/post.rb
class Post < ActiveRecord::Base
  belongs_to :topic
end

Now we are going to create three migrations that will add our foreign keys to our database. If you had wanted to do this when you scaffolded, you could have, though the way we are going about it also teaches you another Rails technique, so I felt that this way would be better. So the create our first migration, run:

script/generate migration add_foreign_to_topics forum_id:integer

This command generates a migration named add_foreign_to_topics, automatically recognizes that we want to change the Topic model, and then we pass what we want to add. In this case we are adding a column named forum_id that is an integer type. To create the migration to add our foreign key of forum_id to our Topic model. Then next command will create the migration

script/generate migration add_foreign_to_posts topic_id:integer

Now we can migrate the database again with:

rake db:migrate

Step 7 - Getting Started on the Home Page

So now we can start on building the functionality of the site. So lets first open up config/routes.rb and add the following line near the commented out lines below:

# config/routes.rb
map.root :controller => "forums"

Lets save the file and delete or rename the file public/index.html. Now we want to open up the view for the index file in app/views/forums/index.html.erb and edit it to the following:

# app/views/forums/index.html.erb
<% title "Forums" %>

<table>
  <tr>
    <th width="70%">Forum</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for forum in @forums %>
    <tr>
      <td><h4><%= link_to h(forum.name), forum_path(forum.id) %></h4>
        <small><%= forum.topics.count %> topics</small><br />
        <%=h forum.description %></td>
      <td class="right"></td>
      <td><%= link_to "Show", forum %></td>
      <td><%= link_to "Edit", edit_forum_path(forum) %></td>
      <td><%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<p><%= link_to "New Forum", new_forum_path %></p>

I changed the table header text into the text that we will be putting in each cell, as well as the width of the cells There are still changes that we are going to make later. Now, open up the application.css file in the public/stylesheets directory. I added the 960.gs CSS Reset and Typography files to the top of the stylesheet. Then I added the following styles to the end of the file:

# public/stylesheets/application.css
table tr th {background:#222; color:#fff; padding:0; border:#111 solid 1px;}
table tr td {border:1px #ccc solid; padding:5px;}
table tr td.right {background: #eee;}
table tr td h4 {margin:0; padding:0; font-weight:normal;}

Now if you want to fire up the server with script/server, you should see the following: (P.S. I added a record into the database just to give an example.)

Now that we have our index action taken care of, lets work on our show action for the forum. This file lies in app/views/forums/show.html.erb. We are going to copy most of the code from our index action into this one, and the final code is below:

# app/views/forums/show.html.erb
<% title @forum.name  %>

<table>
  <tr>
    <th width="60%">Topic Title</th>
    <th width="10%">Replies</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for topic in @forum.topics %>
    <tr>
      <td><%= link_to h(topic.name), topic_path(topic.id) %>
      <td><%= topic.posts - 1 %></td>
      <td class="right"></td>
      <td><%= link_to "Show", topic %></td>
      <td><%= link_to "Edit", edit_topic_path(topic) %></td>
      <td><%= link_to "Destroy", topic, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<p><%= link_to "New Topic", "/topics/new?forum=#{@forum.id}" %></p>

I know I have not explained the code in the two views above, so I'll explain it now. So the first line we set the title for the page. We then create a table and set up the table headers. After, we start a loop for each item in the topics of our forum, and uses the local variable of topic. We then create a link for each topic, then the number of posts, minus 1 to account for the original post. Then we have an extra td that we will use later, and then the admin links, which we will also take care of later.

Step 8 - Working on the Forms

So now that we have the basic layout for our front end. We need to work on the form to create a topic. You should have something that looks like the following:

Well this currently does not work for what we want to do. We currently do not create a post when we create a topic, and we currently give users access to options they should not see. So open up app/views/topics/_form.html.erb. Your current file should look like:

# app/views/topics/_form.html.erb
<% form_for @topic do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :last_poster_id %><br />
    <%= f.text_field :last_poster_id %>
  </p>
  <p>
    <%= f.label :last_post_at %><br />
    <%= f.datetime_select :last_post_at %>
  </p>
  <p><%= f.submit "Submit" %></p>
<% end %>

So we are going to delete the parts with the last_poster_id and last_post_at. We are also going to add in a text area for the post and some hidden fields. You will see that I do not use the Rails helpers, and that is just my personal preference. Your final form should look like:

# app/views/topics/_form.html.erb
<% form_for @topic do |f| %>
  <%= f.error_messages %>
  <% if params[:forum] %><input type="hidden" id="topic_forum_id" name="topic[forum_id]" value="<%= params[:forum] %>" /><% end %>
  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <textarea name="post[content]" cols="80" rows="20"><%= @post.content %></textarea>
  </p>
  <p><%= f.submit "Create" %></p>
<% end %>

And it should look like this in your browser:

We will be saving these in our controller. So, save the view and open up the app/controllers/topic_controller.rb and find the create action. Edit it so it looks like:

# app/controllers/topic_controller.rb
def create
  @topic = Topic.new(params[:topic])
  if @topic.save
    @topic = Topic.new(:name => params[:topic][:name], :last_poster_id => current_user.id, :last_post_at => Time.now, :forum_id => params[:topic][:forum_id])

    if @post.save
      flash[:notice] = "Successfully created topic."
      redirect_to "/forums/#{@topic.forum_id}"
    else
      redirect :action => 'new'
    end
  else
    render :action => 'new'
  end
end

We have added in code to handle creating the post that goes with the topic. Now to make this easier we are going to delete the index action in our controller. We are also going to update the update and destroy actions to redirect properly, so change the redirect_to on these two actions to:

redirect_to "/forums/#{@topic.forum_id}"

Now go ahead and open up app/controllers/posts_controller.rb and delete the index and show actions.

Step 9 - Setting up More Associations

If you noticed that I skipped two associations above, I applaud you on noticing. The associations we skipped were between users and their posts and topics. To do this lets first create two migrations and migrate the changes to the database:

script/generate migration add_user_to_posts user_id:integer
script/generate migration add_user_to_topics user_id:integer
rake db:migrate

Now, to tell ActiveRecord about these associations, open up app/models/user.rb and add the following lines under before_save.

# app/models/user.rb
has_many :posts
has_many :topics

Now open up both app/models/post.rb and app/models/topic.rb and add the following lines:

# app/models/post.rb & app/models/topic.rb
belongs_to :user

Now we have the extra associations to continue.

Step 10 - Polishing up the Forums List

So now that have have the basics now, we are going to continue on and polish up the forums listing, after we will continue on and polish the topics list.

Let's first open up app/models/forum.rb. We are going to add a new method to help us find the most recent post in a forum. The method that we are going to add is below:

# app/models/forum.rb
def most_recent_post
  topic = Topic.first(:order => 'last_post_at DESC', :conditions => ['forum_id = ?', self.id])
  return topic
end

This method finds the first Topic that has a forum ID of whatever the ID of the forum we called this method upon, and then sorts it by date and returns the value.

So open up the app/views/forums/index.html.erb and find the table cell inside of the loop that has a class of right. We are going to change it to the following:

# app/views/forums/index.html.erb
<td class="right"><% if forum.most_recent_post %><%= distance_of_time_in_words_to_now forum.most_recent_post.last_post_at %> ago<% else %>no posts<% end %></td>

This code may look confusing, so I will go through it. First we reference to the method we added to our model, and see if it returned a record, because we may have a forum without any topics. Next, if we have a record, we use the date helpers Rails provides to parse the time into readable text. If we don't have a record returned, we then output 'no posts.' Your page should look like:

Now that we have added the date, we are going to add in the link to the user who posted. Back in the view, change that table cell to look like the following:

# app/views/forums/index.html.erb
<td class="right"><% if forum.most_recent_post %>
  <%= distance_of_time_in_words_to_now forum.most_recent_post.last_post_at %> ago by
  <%= link_to forum.most_recent_post.user.username, "/users/#{forum.most_recent_post.last_poster_id}" %>
  <% else %>no posts<% end %>
</td>

Your page should now look like:

Step 11 - Polish the Topic List

So now we are going to head back to our app/views/forums/show.html.erb file. The title of this step may be a bit confusing due to the fact that we are editing the forum's show action, but actually working on the Topics list. Now that you have the file open, find the table cell inside the look with the class of 'right' again. We are going to be using the same date helper, and our code will look like:

# app/views/forums/show.html.erb
<td class="right"><%= distance_of_time_in_words_to_now topic.last_post_at %> ago by <%= link_to topic.user.username, "/users/#{topic.last_poster_id}" %></td>

Now if you open up your browser, you should see:

Step 12 - Working on the Topic View

So now that we have everything in the Forum and Topic lists sorted out, we are going to work on the third major view. Go ahead and open up app/views/topics/show.html.erb and edit it so it looks more like:

# app/views/topics/show.html.erb
<% title @topic.name %>

<% for post in @topic.posts %>
<div class="post">
  <span class="left"><%= post.user.username %><br /><%= link_to "Delete", post, :confirm => 'Are you sute?', :method => :delete%></span>
  <span class="right"><%= post.content %></span>
</div>
<% end %>

<p>
  <%= link_to "Edit", edit_topic_path(@topic) %> |
  <%= link_to "Destroy Topic", @topic, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", topics_path %>
</p>

Let's also add in some styling:

# public/stylesheets/application.css
.post {width:100%; display:table;}
.post span {padding:5px; border:3px solid #eee; margin:0;}
.post span.left {width:150px; background:#eee; border:3px solid #eee; display:table-cell;}
.post span.right { border:3px solid #eee; display:table-cell;}

And now viewing a topic will look like this:

Step 13 - User Show Action

When a username is clicked on, we want to show their profile, correct? So to do this open up the app/controllers/users_controller.rb and and the show action below:

# app/controllers/users_controller.rb
def show
  @user = User.find(params[:id])
end

That is all we will need for this action in our controller. Now create a file named show.html.erb in app/views/users, and place the following inside:

# app/views/users/show.html.erb
<% title 'user: ' + @user.username %>
<div id="topics">
  <strong>topics</strong>
  <% @user.topics.each do |topic| %>
    <%= link_to topic.name, topic_url(topic.forum.id) %>
  <% end %>
</div>

That simple amount of code will display the user's topics. No, there is not a limit here so we would technically show all of the topics by that user. The most ideal solution would be to create methods to handle this in our User model, but I will not do that here.

Step 14 - Working With Posts

So now that we are almost done, we need to work on the functions to edit a post. Let's first open up app/controllers/topics_controller.rb and our view for the show action. First, on the controller, we are going to delete the edit and update functionality. You may wonder why, and the reason is that we are not going to allow a user to edit the topic, but rather posts. Yes, this does not allow a user to change the title of a topic but that is better than writing the extra logic required to edit a topic. So go ahead and delete those actions, and you can also delete the corresponding views.

Now open up the app/views/topics/show.html.erb file. First, in the paragraph tag at the bottom, delete the Edit link, the first one in the list. Then go up to the loop, and before the link to delete the post, we are going to add in a link to edit the post. The view when we are done should look like:

# app/views/topics/show.html.erb
<% title @topic.name %>

<% for post in @topic.posts %>
<div class="post">
  <span class="left"><%= link_to post.user.username, user_url(post.user) %><br /><%= link_to "Edit", edit_post_path(post) %><br /><%= link_to "Delete", post, :confirm => 'Are you sute?', :method => :delete %></span>
  <span class="right"><%= post.content %></span>
</div>
<% end %>

<p>
  <%= link_to "Reply", "#{new_post_path}?topic=#{@topic.id}" %> |
  <%= link_to "Destroy Topic", @topic, :confirm => 'Are you sure?', :method => :delete %> |
  <%= link_to "View All", forum_path(@topic.forum_id) %>
</p>

We also added a reply link for functionality we will add in a minute. Now, when you click on the link to edit, it should work. Then if you try and change the post, Rails gives you an error. Well, we don't want to have it redirecting where it is currently, so open up app/controllers/posts_controller.rb, scroll down to the create action, and find the line "redirect_to @post." We are going to change the line so the final action looks like:

# app/controllers/posts_controller.rb
def create
  @post = Post.new(:content => params[:post][:content], :topic_id => params[:post][:topic_id], :user_id => current_user.id)
  if @post.save
    flash[:notice] = "Successfully created post."
    redirect_to "/topics/#{@post.topic_id}"
  else
    render :action => 'new'
  end
end

So that code will redirect us now to the show action for the forum id of the post. We only have one problem though, the create action is not passed the topic_id in the view, so open up app/views/posts/_form.html.erb and add the following line under the error messages.

# app/views/posts/_form.html.erb
<% if params[:topic] %><input type="hidden" id="topic_forum_id" name="post[topic_id]" value="<%= params[:topic] %>" /><% end %>

So now that we also need to update the Topic that we have a new post. So to do this, we are going to again change the create action to look like:

# app/controllers/posts_controller.rb
def create
  @post = Post.new(:content => params[:post][:content], :topic_id => params[:post][:topic_id], :user_id => current_user.id)
  if @post.save
    @topic = Topic.find(@post.topic_id)
    @topic.update_attributes(:last_poster_id => current_user.id, :last_post_at => Time.now)
    flash[:notice] = "Successfully created post."
    redirect_to "/topics/#{@post.topic_id}"
  else
    render :action => 'new'
  end
end

What we did was after the post was saved, we used the topic_id to find our topic, and then update two columns in our database. Go ahead and copy those two lines that we added into the same place on our update action, so it looks like:

# app/controllers/posts_controller.rb
def update
  @post = Post.find(params[:id])
  if @post.update_attributes(params[:post])
    @topic = Topic.find(@post.topic_id)
    @topic.update_attributes(:last_poster_id => current_user.id, :last_post_at => Time.now)
    flash[:notice] = "Successfully updated post."
    redirect_to @post
  else
    render :action => 'edit'
  end
end

We also changed the redirect on this one so that it does the same as the create action. Now go down to the destroy action, and change the redirect to:

redirect_to forums_url

Step 15 - Putting the Final Touches on the Forum

Now, we need to do some quick cleaning up in our views for links that are unnecessary or no longer work. First up, the show action for the forums, so open up app/views/forums/show.html.erb and delete the show and edit links so that the view looks like:

# app/views/forums/show.html.erb
<% title @forum.name  %>

<table>
  <tr>
    <th width="60%">Topic Title</th>
	<th width="10%">Replies</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for topic in @forum.topics %>
    <tr>
      <td><%= link_to h(topic.name), topic_path(topic.id) %>
	  <td><%= topic.posts.count - 1%></td>
      <td class="right"><%= distance_of_time_in_words_to_now topic.last_post_at %> ago by <%= link_to topic.user.username, "/users/#{topic.last_poster_id}" %></td>
	  <td><%= link_to "Destroy", topic, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<p><%= link_to "New Topic", "/topics/new?forum=#{@forum.id}" %></p>

We also need to update the Topic creation method so that it adds in the user_id of our current user, so open up app/controllers/topics_controller.rb and change the Topic.new line to:

# app/controllers/topics_controller.rb
@topic = Topic.new(:name => params[:topic][:name], :last_poster_id => current_user.id, :last_post_at => Time.now, :forum_id => params[:topic][:forum_id], :user_id => current_user.id)

We also need to clean up the app/views/forums/index.html.erb file, so go ahead and open it up and delete the show link so the file looks like:

# app/views/forums/index.html.erb
<% title "Forums" %>

<table>
  <tr>
    <th width="70%">Forum</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for forum in @forums %>
    <tr>
      <td><h4><%= link_to h(forum.name), forum_path(forum.id) %></h4>
        <small><%= forum.topics.count %> topics</small><br />
        <%=h forum.description %></td>
      <td class="right"><% if forum.most_recent_post %><%= distance_of_time_in_words_to_now forum.most_recent_post.last_post_at %> ago by <%= link_to forum.most_recent_post.user.username, "/users/#{forum.most_recent_post.last_poster_id}" %><% else %>no posts<% end %></td>
      <td><%= link_to "Edit", edit_forum_path(forum) %></td>
      <td><%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<p><%= link_to "New Forum", new_forum_path %></p>

Step 16 - Administrative Functions

Currently, any user can edit, delete, and create anything they want. So we are going to open up our Application helper in app/helpers/application_helper.rb. We are going the add in a few extra methods to help us deal with authentication and user levels. Before the 'end' tag, add these methods:

# app/helpers/application_helper.rb
def admin?
  if current_user.permission_level == 1 || current_user.id == 1
    return true
  else
    return false
  end
end

def owner?(id)
  if current_user.id == id
    return true
  else
    return false
  end
end

def admin_or_owner?(id)
  if (admin? || owner?(id))
    return true
  else
    return false
  end
end

These methods allow us is find out if a user is an admin, and owner by passing in the user_id of what we are looking for, and one that handles both. The method checking for an admin also allows the first user to be an admin automatically. So we are first going to open up the Forum index.html.erb file. We are going to add in some extra code now to hide the create, edit, and destroy forum links from anyone but admins. We are going to wrap each of these links with <% if admin? %> and <% end %>. The final code will look like:

# app/views/forums/index.html.erb
<% title "Forums" %>

<table>
  <tr>
    <th width="70%">Forum</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for forum in @forums %>
    <tr>
      <td><h4><%= link_to h(forum.name), forum_path(forum.id) %></h4>
        <small><%= forum.topics.count %> topics</small><br />
        <%=h forum.description %></td>
      <td class="right"><% if forum.most_recent_post %><%= distance_of_time_in_words_to_now forum.most_recent_post.last_post_at %> ago by <%= link_to forum.most_recent_post.user.username, "/users/#{forum.most_recent_post.last_poster_id}" %><% else %>no posts<% end %></td>
      <% if admin? %><td><%= link_to "Edit", edit_forum_path(forum) %><% end %></td><% end %>
      <% if admin? %><td><%= link_to "Destroy", forum, :confirm => 'Are you sure?', :method => :delete %></td><% end %>
    </tr>
  <% end %>
</table>

<p><% if admin? %><%= link_to "New Forum", new_forum_path %></p>

Now if you try and reload the page, you will receive and error saying "undefined method `permission_level'." The error is telling you that there is no column named permission_level. We need to create a migration, so run the following commands to create and migrate the changes:

script/generate migration add_permissions_to_users permission_level:integer
rake db:migrate

Now, everything should work properly. Now, we are going to move onto the show action, so open up app/views/forums/show.html.erb, and edit it to look like the following:

# app/views/forums/show.html.erb
<% title @forum.name  %>

<table>
  <tr>
    <th width="60%">Topic Title</th>
	<th width="10%">Replies</th>
    <th width="30%">Last Post</th>
  </tr>
  <% for topic in @forum.topics %>
    <tr>
      <td><%= link_to h(topic.name), topic_path(topic.id) %>
      <td><%= topic.posts.count - 1%></td>
      <td class="right"><%= distance_of_time_in_words_to_now topic.last_post_at %> ago by <%= link_to topic.user.username, "/users/#{topic.last_poster_id}" %></td>
      <% if admin? %><td><%= link_to "Destroy", topic, :confirm => 'Are you sure?', :method => :delete %></td><% end %>
    </tr>
  <% end %>
</table>

<p><% if logged_in? %><%= link_to "New Topic", "/topics/new?forum=#{@forum.id}" %><% end %></p>

What we did was make the destroy action admin only, and the create topic action logged in only. Now, we are going to work on the Topic show action, the file being app/views/topics/show.html.erb. Open it up and edit it so it looks like:

# app/views/topics/show.html.erb
<% title @topic.name %>

<% for post in @topic.posts %>
<div class="post">
  <span class="left"><%= link_to post.user.username, user_url(post.user) %><br /><% if logged_in? %><% if admin_or_owner?(post.user.id) %><%= link_to "Edit", edit_post_path(post) %><br /><%= link_to "Delete", post, :confirm => 'Are you sute?', :method => :delete %><% end %><% end %></span>
  <span class="right"><%= post.content %></span>
</div>
<% end %>

<p>
  <% if logged_in? %><%= link_to "Reply", "#{new_post_path}?topic=#{@topic.id}" %> |<% end %>
  <% if admin? %><%= link_to "Destroy Topic", @topic, :confirm => 'Are you sure?', :method => :delete %> |<% end %>
  <%= link_to "View All", forum_path(@topic.forum_id) %>
</p>

We again just implemented some methods so that unregistered users and users that do not have enough permissions are unalbe to see some links. Now that we have the links locked down, we are going to lock down the controllers, so first open up app/controllers/forum_controller.rb and at the op right under the class declaration, add:

  before_filter :admin_required, :except => [:index, :show]

So, now if you try your application, you will notice that Rails will give us an error, and that is because he have not defined admin_required. To add this, we are going to define it in our application controller, so open it up and add the following method right before the last end tag:

# app/controllers/application_controller.rb
def admin_required
  unless current_user && (current_user.permission_level == 1 || current_user.id == 1)
    redirect_to '/'
  end
end

This says that unless we are an admin, redirect to the home page. We now are going to continue to our topics controller, just like with the forums contoller, we are going to add some before filters, and this time, add:

# app/controllers/topics_controller.rb
before_filter :login_required, :except => [:index, :show]
before_filter :admin_required, :only => :destroy

So what we are saying is that you must be logged in for every action except index and show, and you must be an admin to access the destroy action. Next, we are going to work on our posts controller. This one is going to be a tad more work. First, add the before filter:

# app/controllers/posts_controller.rb
before_filter :login_required

Next, we are going to add in an extra method to our application_controller.rb file to handle this. Here is the method we are going to add:

# app/controllers/application_controller.rb
def admin_or_owner_required(id)
  unless current_user.id == id || current_user.permission_level == 1 || current_user.id == 1
    redirect_to '/'
  end
end

This method checks to see if the user_id passed to it equals our current user's id, or if our current user is an admin. Back to our posts controller, and edit the actions of edit, update, and destroy so they look like:

# app/controllers/posts_controller.rb
def edit
  @post = Post.find(params[:id])
  admin_or_owner_required(@post.user.id)
end

def update
  @post = Post.find(params[:id])
  admin_or_owner_required(@post.user.id)
  if @post.update_attributes(params[:post])
    @topic = Topic.find(@post.topic_id)
    @topic.update_attributes(:last_poster_id => current_user.id, :last_post_at => Time.now)
    flash[:notice] = "Successfully updated post."
    redirect_to "/topics/#{@post.topic_id}"
  else
    render :action => 'edit'
  end
end

def destroy
  @post = Post.find(params[:id])
  admin_or_owner_required(@post.user.id)
  @post.destroy
  flash[:notice] = "Successfully destroyed post."
  redirect_to forums_url
end

Now that we have the authentication done, we have one last step.

Step 17 - Finishing the Application Layout

So now we are going to add a header to our application layout file. Go ahead an open up app/views/layouts/application.html.erb and add the following right after the body tag:

# app/views/layouts/application.html.erb
<div id="header">
	<h1>My Forums!</h1>
	<% if logged_in? %>
		Welcome <%= current_user.username %>! Not you?
		<%= link_to "Log out", logout_path %>
	<% else %>
		<%= link_to "Sign up", signup_path %> or
		<%= link_to "log in", login_path %>.
	<% end %>
</div>

Now open up our CSS file and add the following line:

# public/stylesheets/application.css
#header {width:75%; margin:0 auto;}

Opening up your web browser, you should see something like the following:

Conclusion

I hope you enjoyed this tutorial, and if you made your way through this monstrosity, I commend you! If you are looking for a forum, this wouldn't be a bad start, but Beast (altered_beast in this case) is a great forum with a really nice and smooth design. Below is a screen with stats about our program, and it took 276 lines of code (excluding views) to build this forum!


Related Posts
  • Web Design
    UX
    Impress Your Visitors With a Web 2.0 Hit CounterCounter thumb
    If there's one way to show off how successful your site is, it's by letting your visitors know how many others have been there before them. Let's furnish a web page with a hit counter!Read More…
  • Web Design
    eCommerce
    Taking Shopify Theme Development FurtherShopify thumb
    In this final part of our Shopify series we will look at how a deeper knowledge of Liquid will enable you to make your themes even more flexible. This will help you deliver richer and more imaginative store designs.Read More…
  • Code
    JavaScript & AJAX
    Working With IndexedDB - Part 3Indexeddb retina preview
    Welcome to the final part of my IndexedDB series. When I began this series my intent was to explain a technology that is not always the most... friendly one to work with. In fact, when I first tried working with IndexedDB, last year, my initial reaction was somewhat negative ("Somewhat negative" much like the Universe is "somewhat old."). It's been a long journey, but I finally feel somewhat comfortable working with IndexedDB and I respect what it allows. It is still a technology that can't be used everywhere (it sadly missed being added to iOS7), but I truly believe it is a technology folks can learn and make use of today. In this final article, we're going to demonstrate some additional concepts that build upon the "full" demo we built in the last article. To be clear, you must be caught up on the series or this entry will be difficult to follow, so you may also want to check out part one.Read More…
  • Web Design
    Email
    Creating a Simple Responsive HTML EmailEmail new rwd thumb
    In this tutorial I will show you how to create a simple responsive HTML email which will work in every email client, including all the new smartphone mail clients and apps. It uses minimal media queries and a fluid width approach to ensure maximum compatibility.Read More…
  • Code
    JavaScript & AJAX
    Working With IndexedDB - Part 2Indexeddb retina preview
    Welcome to the second part of my IndexedDB article. I strongly recommend reading the first article in this series, as I'll be assuming you are familiar with all the concepts covered so far. In this article, we're going to wrap up the CRUD aspects we didn't finish before (specifically updating and deleting content), and then demonstrate a real world application that we will use to demonstrate other concepts in the final article.Read More…
  • Code
    HTML & CSS
    Pure: What, Why, & How?Pure retina preview
    This tutorial will introduce you to Pure, a CSS library made of small modules, that can help you in writing completely responsive layouts, in a very fast and easy way. Along the way, I'll guide you through the creation of a simple page in order to highlight how you can use some of the library's components.Read More…