Adam O'Grady

Rails Blog: Part 2

Continuing on from the first part of the Rails Blog tutorial we’re going to flesh out our blog concept even further. Now we want to create an administrator role who has permissions to edit/delete other’s posts in case they contain unsuitable material or are outdated and the connected account is deleted. There’s a few possible ways of doing this, but what we’re going to do is add a boolean attribute to the user model that signifies whether a user is an administrator.

Firstly, run rake db:reset to reset the database, which we’ll need to do because we’re going to set the first registered user of the blog/new-site to be an administrator and they can promote other admins afterwards. Next run rails g migration AddAdminToUsers admin:boolean then open the newly created migration, it should be in db/migrate/YYYYMMDDHHMMSS_add_admin_to_users.rb with it’s own timestamp in the title. Change the change method to look like the below (which gives a default value of false for administrator):

def change
  add_column :users, :admin, :boolean, :default => false
end

Then run rake db:migrate to assign the changes to the schema and database. Next, go into app/controllers/posts_controller.rb and in the index method add the following before the end keyword:

if current_user == User.first && !current_user.try(:admin?) && User.count == 1
  current_user.update_attribute :admin, true
end

This checks if the user is the first user in the database, that they’re not already an admin and that they’re the only user in the database and if all three conditions are met (ie: the blog has just been set up) it sets the current user as an admin. Now we can test for if the current user is an admin with this: current_user.try(:admin?), which returns true if the user is an admin and false if the current user is not an admin or there is no current user. If you’re certain that a value contains a user, you can also user @user.admin?. So now we need to add some logic to the controller to allow the admins to do their work:

# GET /posts/1/edit
def edit
  if author_exists = User.where(:id => @post.user_id).first
    if current_user == author_exists || current_user.try(:admin?)
    else
      render :show
    end
  else
    if current_user.try(:admin?)
    else
      render :show
    end
  end
end

# PATCH/PUT /posts/1
# PATCH/PUT /posts/1.json
def update
  if author_exists = User.where(:id => @post.user_id).first
    if current_user == author_exists || current_user.try(:admin?)
      respond_to do |format|
        if @post.update(post_params)
          format.html { redirect_to @post, notice: 'Post was successfully updated.' }
          format.json { render :show, status: :ok, location: @post }
        else
          format.html { render :edit }
          format.json { render json: @post.errors, status: :unprocessable_entity }
        end
      end
    else
      render :show
    end
  else
    if current_user.try(:admin?)
      if @post.update(post_params)
        redirect_to @post, notice: 'Post was successfully updated.'
      else
        render :edit
      end
    else
      render :show
    end
  end
end

# DELETE /posts/1
# DELETE /posts/1.json
def destroy
  if author_exists = User.where(:id => @post.user_id).first
    if current_user == author_exists || current_user.try(:admin?)
      @post.destroy
      respond_to do |format|
        format.html { redirect_to posts_url, notice: 'Post was successfully destroyed.' }
        format.json { head :no_content }
      end
    else
      render :show
    end
  else
    if current_user.try(:admin?)
      @post.destroy
      redirect_to posts_url, notice: 'Post was successfully destroyed.'
    else
      render :show
    end
  end
end

For each method, it checks if the author of the post exists and if so, performs an action if the current user is the author OR the current user is an admin; in the case that there is no existing author an admin can still perform the appropriate actions on the post.

Next we’ll need to open up app/views/posts/index.html and edit the section with the Edit/Destroy links to look like so:

<% if (current_user == post.user && post.user != nil) || current_user.try(:admin?) %>
  <td><%= link_to 'Edit', edit_post_path(post) %></td>
  <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>

This makes it so if the current user is the post owner OR an administrator they can edit and destroy the posts through the view.

We’ll also need a way of promoting new admins and to do this we’ll need to create a User controller. Now Devise performs some controls and gives some routes, but we need to create our own controller to add some actions. So run rails g controller users, then open up config/routes.rb and add resources :users after the other resource routes. Next open up the new app/controllers/users_controller.rb and make it look like:

class UsersController < ApplicationController
  before_filter :authenticate_user!

  def index
    @users = User.all
  end

  def show
    @user = User.find(params[:id])
  end

  def update
    @user = User.find(params[:id])
    if current_user.try(:admin?)
      if current_user == @user && params[:user][:admin] == '0' && User.where(:admin => true).count == 1
        render :show
      else
        if @user.update(user_params)
          redirect_to user_path(@user), notice: 'User was successfully updated.'
        else
          render :show
        end
      end
    else
      render :show
    end
  end

  def destroy
    if current_user.try(:admin?)
      @user = User.find(params[:id])
      if current_user == @user && User.where(:admin => true).count == 1
        render :show
      else
        @user.destroy
        redirect_to users_path
      end
    else
      render :show
    end
  end

  private
    def user_params
      params.require(:user).permit(:admin)
    end
end

The obvious classes (index, show) simply show all or a select User. update checks if the current_user is an admin and if they are, updates the selected user unless they happen to be the selected user, they’re disabling admin and there’s only one admin currently available (to prevent a “no admin on blog” situation occurring). destroy checks if the user is an admin and if so, deletes the selected user unless they are the selected user and there’s only one admin on the board.

Next we’ll need to create some views to see this, so create app/views/users/index.html.erb and fill it with:

<h1>Listing users</h1>

<table width="100%">
  <tr>
    <td width="30%"><strong>Email</strong></td>
    <td width="10%"><strong>Admin</strong></td>
    <td colspan="2" width="20%"></td>
  </tr>

  <% @users.each do |user| %>
    <tr>
      <td><%= link_to user.email, user_path(user) %></td>
      <td>
        <% if user.admin? %>
          Yes
        <% end %>
      </td>
      <% if current_user.try(:admin?) %>
        <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      <% else %>
        <td></td>
      <% end %>
    </tr>
  <% end %>
</table>

<p>
  <%= link_to 'Index', posts_path %>
</p>

This iterates through the users and displays them all, also showing if they’re admins and allowing admins to delete users (under the constraints set out in the users controller). Also create app/views/users/show.html.erb and set it’s contents to be:

<h1><%= @user.email %></h1>
<p>
  <% if current_user.try(:admin?) %>
    <%= form_for @user, url: user_path(@user) do |f| %>
      <p>
        <%= f.label :admin %>
        <%= f.check_box(:admin) %>
      </p>
      <%= f.submit %>
    <% end %>
  <% end %>
</p>

<p>
  <%= link_to 'Users List', users_path %>
</p>

This is where an admin can mark other users as admins or not, simple really. Lastly we’ll need to open up app/views/posts/index.html.erb and change the last few lines to the following:

<p>
  <%= link_to 'New Post', new_post_path %>
</p>

<p>
  <%= link_to 'Users', users_path %>
</p>

This gives us a link to the users section that people can use to view current users and admins can use to admin!

Now you should have a working administrator model with protections/capabilities all set out. You can apply the same ideas here to things such as the Rails Forum Skeleton project (Part 1, Part 2, Part 3, Thoughts) and also expand it to create other roles including moderator or banned or whatever else you need.