Adam O'Grady

Rails Forum Skeleton: Part 2

Carrying on from my last tutorial about building the relationships and core view elements for discussions and posts, we’re going to extend our budding forum by adding users and authentication.

To start, open Gemfile and down the bottom add gem 'devise' then run bundle install. Devise is an all-in-one authentication system that handles registration, sessions, authentication, recovering accounts, etc. To set up devise (our authentication station), run rails g devise:install. We’ll need to set the config.action_mailer.default_url_options option so open config/environments/development.rb and before the end keyword add config.action_mailer.default_url_options = { :host => 'localhost:3000' }.

To create the user model which utilises the features of the devise install, run the command rails g devise user. You’ll also need to run rake db:migrate afterwards. Devise will handle the routes (routes are the way Rails handles directing incoming traffic that’s destined for different controllers) and code for creating accounts, logging in/out and so forth and by now (if you’ve restarted your Rails server) you should be able to create an account at http://localhost:3000/users/sign_up. If you do create an account, you’ll soon notice it tries to access the root path, so open up config/routes.rb and add root 'discussions#index' under the resources listed. This turns the root path (http://localhost:3000/ in our case) into a view of all the discussions.

In /app/views/layouts/application.html.erb, underneath the opening body tag add the following code:

<div>
    <% if user_signed_in? %>
        Logged in as <strong><%= current_user.email %></strong>.
        <%= link_to 'Edit profile', edit_user_registration_path %> |
        <%= link_to "Logout", destroy_user_session_path, method: :delete %>
    <% else %>
        <%= link_to "Sign up", new_user_registration_path %> |
        <%= link_to "Login", new_user_session_path %>
    <% end %>
</div>

This code checks if a user is signed in and if so, shows who is logged in and provides links to edit their profile or logout. If no one is logged in, it shows sign up/login links.

At the top of both app/controllers/discussions_controller.rb and app/controllers/posts_controller.rb, underneath the class head but above the before_action line add the following which prevents people from creating/editing/deleting discussions or posts unless they are logged in:

before_filter :authenticate_user!, :only => [:new, :edit, :create, :update, :destroy]

We’ll also need to add associations/references between posts, discussions and users. Run the following commands from the prompt:

$ rails generate migration add_user_to_posts user_id:integer
$ rails generate migration add_user_to_discussions user_id:integer
$ rake db:migrate

Add the following lines before the end keyword in app/models/user.rb:

has_many :posts
has_many :discussions

And in both app/models/discussion.rb and app/models/post.rb add belongs_to :user before the end keyword. This links all our models up quite nicely.

In app/views/posts/_form.html.erb and app/views/discussions/_form.html.erb we’ll want to add <%= f.hidden_field :user_id, value: current_user.id %> before the submit button. This provides the receiving controller method with the user id of the currently logged in user. In the discussions controller, add @post.user_id = @discussion.user_id under @post.discussion_id = @discussion.id in the create method.

Still in the discussions controller, modify the edit method so it looks like so:

if current_user == User.find(@discussion.user_id)
else
    redirect_to ideas_path
end

And then change the update and destroy methods to look like this:

# PATCH/PUT /discussions/1
def update
    if current_user == User.find(@discussion.user_id)
        @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
    else
        redirect_to ideas_path
    end
end


# DELETE /discussions/1
def destroy
    if current_user == User.find(@discussion.user_id)
        @discussion.destroy
        redirect_to discussions_url, notice: 'Discussion was successfully destroyed.'
    else
        redirect_to ideas_url
    end
end

These checks help enforce some integrity, making it so that only the correctly logged in user (or an administrator) can edit their own posts/discussions.

In the posts controller (app/controllers/posts_controller.rb), do some similar magic to your edit, update, destroy methods so they look like such:

# GET /posts/1/edit
def edit
    if current_user == User.find(@post.user_id)
    else
        redirect_to ideas_path
    end
end

    # PATCH/PUT /posts/1
# PATCH/PUT /posts/1.json
def update
    if current_user == User.find(@post.user_id)
        if @post.update(post_params)
            redirect_to discussion_url(@post.discussion_id), notice: 'Post was successfully updated.'
        else
            render :edit
        end
    else
        redirect_to discussion_url(@post.discussion_id)
    end
end

# DELETE /posts/1
# DELETE /posts/1.json
def destroy
    if current_user == User.find(@post.user_id)
        @post.destroy
        redirect_to discussions_url, notice: 'Post was successfully destroyed.'
    else
        redirect_to discussions_url
    end
end

We’re nearing the end, next you’ll need to edit the discussion_params and post_params private methods in the discussions and posts controllers respectively. They should end up looking like so:

def discussion_params
    params.require(:discussion).permit(:title, :user_id)
end

def post_params
    params.require(:post).permit(:content, :user_id)
end

This allows the user_id parameter from our edit/new forms to come through to the update/create methods and be used as a property of them.

Well, how’s that feel? We’ve now added user authentication and some basic permissions/ownership to our forum!If you want to take it further, check out Part 3 for adding username authentication and Gravatar support.