Adam O'Grady

Rails Forum Skeleton: Part 1

Here’s a quick tutorial on how to create a framework for a forum. It doesn’t get into creating users, authentication or permissions, it’s mostly to show the relationships between entities and shore up the right views.

Preface: So in the quest for new projects I’ve considered building an all-in-one website-forum-chatroom package with the goal of creating something that people can download to their server, run an install script and it sets up everything necessary and then allows the user to do post-install customisation (setting up static website pages, creating user permission levels, forum headings, etc). Pretty massive task though, so it’s not a project I’m putting a heap of stock in at the moment. For now, I’m comforting myself by learning how to develop a basic forum in Ruby on Rails which may eventually become a part of the larger project.

For this tutorial I used Ruby 2.1.1 or greater and Rails 4.1.1 so make sure you’re using something equivalent. Once you’ve created your new forum project and entered its directory with rails new forum && cd forum, we’ll need to set up some scaffolding. This is a simple, beginner framework for modelling some of the data we’ll be using. It consists of models, views and controllers, which make up “MVC” pattern frameworks. In short, models refer to the data and the logic that holds it together, views represent what a user sees and enters, accepting input and displaying output while controllers take input and advise models to update data and also render/direct the user to the appropriate view. A lot of the views and a number of controller methods created by the scaffolding are unused and you can remove them later, but it’s good to keep them for testing until you’re sure everything is working correctly. To set up the scaffolding, enter the following commands:

$ rails g scaffold discussion title:string
$ rails g scaffold post content:text

Now create a database migration (used by Rails as a way of updating tables or attributes in the database) where you add a foreign key (reference in one table to another table) to posts, referring to a discussion.

$ rails g migration add_foreign_to_posts discussion_id:integer
$ rake db:migrate

Open up the discussion model (app/models/discussion.rb) and edit it to reflect this:

    class Discussion < ActiveRecord::Base
            has_many :posts, :dependent => :destroy
    end

And for the post model (app/models/post.rb):

    class Post < ActiveRecord::Base
            belongs_to :topic
    end

These changes help reflect the relationship between discussions and posts, a one-to-many system where each discussion can have many post but each post belongs to only one discussion. It’s also set so when a discussion is deleted, all dependent posts are deleted as well.

Open up the _form partial for discussions (app/views/discussions/_form.html.erb). After the text label and field for the new form, add an entry for post content like so:

<div class="field">
    <%= f.label :title %><br>
    <%= f.text_field :title %>
</div>
<div class="field">
    <textarea name="post[content]" cols="80" rows="20"><%= @post.content %></textarea>
</div>
<div class="actions">
    <%= f.submit %>
</div>

When the form is submitted, it will now have a parameter for for post content in the form of params[:post][:content]. Open up the posts controller (app/controllers/post_controller.rb) and update the edit, create and update methods.

# GET /discussions/1/edit
def edit
    @discussion = Discussion.find(params[:id])
    @post = @discussion.posts.first
end

# GET /discussions/new
def new
    @discussion = Discussion.new
    @post = Post.new
end

# POST /discussions
def create
    @discussion = Discussion.new(discussion_params)
    @post = Post.new(post_params_on_discussion)

    if @discussion.save
        @post.discussion_id = @discussion.id
        if @post.save
        redirect_to @discussion, notice: 'Discussion was successfully created.'
        else
            render :new
        end
    else
        render :new
    end
end

# PATCH/PUT /discussions/1
def update
    @post = @discussion.posts.first
    if @discussion.update(discussion_params)
        if @post.update(post_params_on_discussion)
            redirect_to @discussion, notice: 'Discussion was successfully updated.'
        else
            render :edit
        end
    else
        render :edit
    end
end

You’ll also need to add a private helper (post_params_on_discussion) like so, this is because Rails 4.* strong parameters prevent one from using unchecked database input in mass-assignments. This makes sure there is a :post parameter and only allows the sub-parameter :content through:

def private
    def post_params_on_discussion
            params.require(:post).permit(:content)
    end
end

In the show view for discussions (app/views/discussions/show.html.erb) we’ll need to add some stuff under the paragraph tags containing the title:

<p>
    <strong>Title:</strong>
    <%= @discussion.title %>
</p>

<% for post in @discussion.posts %>
    <div>
        <%= post.content %>
    </div>
    <div>
        <%if post.id == @discussion.posts.first.id %>
            <%= link_to "Edit", edit_discussion_path(@discussion) %>
            <%= link_to "Destroy Thread", discussion_path(@discussion), method: :delete, data: { confirm: "Are you sure?" } %>
        <% else %>
            <%= link_to "Edit", edit_post_path(post) %>
            <%= link_to "Destroy Post", post, method: :delete, data: { confirm: "Are you sure?" } %>
        <% end %>
    </div>
<br /><br />
<% end %>

<%= link_to "Reply", "#{new_post_path}?discussion=#{@discussion.id}" %><br />

This lists all posts connected to that discussion (thanks to the has_many/belongs_to relationship we created earlier) and gives an option to edit or delete them; with the exception of the first post which allows us to edit/delete the entire discussion as well. Now if you check out apps/views/discussions/edit.html.erb you’ll see it also calls the app/views/discussions/_form.html.erb partial which we edited to include a “@post.content” text area. And to top it off, in app/controllers/discussions_controller.rb we changed the edit method to provide the first post connected to our discussion and updated the update method to save the post details, covering all our bases there for updating the discussion and it’s connected first post!

The reply section at the bottom will become clearer soon, but effectively it sends us to a new post page and gives us parameter called “discussion” which is equal to the discussion id.

I’ll get you now to open up app/controllers/post_controller.rb and in the create and update methods, change the redirect_to and make it point at discussion_url(@post.discussion_id). This means when you finish creating or updating a post it takes you back to the discussion, which is the more logical option for a forum then taking you to just view the post.

Also set it so when you destroy a post, it takes you to discussions_url, the index for the discussions rather than the posts (we don’t want users ending up at a view of all the unordered posts, not connected to discussions).

At the base of the app/views/posts/_form.html.erb but above the closing <% end %> tag, add the following:

<% if params[:discussion] %><input type="hidden" id="discussion_forum_id" name="post[discussion_id]" value="<%= params[:discussion] %>" /><% end %>

This takes the discussion parameter mentioned earlier (from app/views/discussions/show.html.erb) and turns it into a hidden attribute for post referring to the posts discussion id. This helps with the redirects back to the discussion when posting/editing posts.

In app/views/posts/edit.html.erb change the Back link to point to discussion_url(@post.discussion_id). In app/views/posts/new.html.erb point the Back link at discussion_url(params[:discussion]) (referring to the parameter passed to it once more). This should clean up the links when editing or creating posts.

Now you should have a basic forum framework in terms of relationships between major entities (discussions/posts) and MVC components set up that allow you to create new discussions, add posts to them, edit the posts (and initial post/discussion title) without taking you to views you wouldn’t expect a forum to take you (such as a listing of all posts, etc).

Good luck!