Let's make an admin interface for Rails 3.1. Not by using a complete product like RailsAdmin, but by making our own.

This article is quite old.Time flies when you're having fun. I've been writing for my blog for a long time. Stuff changes fast, especially in the Ruby world. That's why I've put this warning on old posts. The article might still be valid, though.

Backends in Rails 3.1

If you find yourself needing a backend interface, you can either get an admin plugin of the shelf, like RailsAdmin or ActiveAdmin, or build your own. The of-the-shelf options provide a lot of functionality and are really worth a look.

You can also build your own backend. Building your own gives you maximum freedom. It requires a bit more work, but it might be preferrable, especially if your customer also needs to work with it.

There are some awesome tools out there to help you build your own backend. I'll show you how I would go about making such a backend. I'm using Rails 3.1 (the rc5 at this moment) on Ruby 1.9.2. I might gloss over some details here and there, so use your own expertise to fill in some gaps.

The entire backend is available on Github. I have also published the end result on Heroku You can log in with username "frodo" and the password "thering".

A new application controller

A backend can be seen as a different application inside your regular application. That's why I start out with a fresh application controller, just for the backend, from which every other controller can inherit.

rails generate controller backend/application

Because I don't want to be bothered by anything from the normal application controller, I choose to inherit straight from ActionController::Base:

class Backend::ApplicationController < ActionController::Base
  protect_from_forgery
end

All routes should be in the backend too. This is extremely simple. Just wrap your normal routes inside a namespace block:

SimpleBackendExample::Application.routes.draw do

  root to: "home#index"
  # other routes go here

  namespace :backend do
    root to: "products#index"
    resources :products
    resources :widgets
  end

end

Now it's time to implement these resources.

Resources

Usually, you'll need a lot of CRUD resources inside your backend. I prefer to use the inherited_resources gem.

First, make sure you install it, by adding it to your Gemfile and running the bundle command:

gem 'inherited_resources'

Next, I prefer to make a base class for all resource controllers too. This is the ideal place for configuring Inherited Resources. Every CRUD controller can inherit from this one, while other controllers can inherit from Backend::ApplicationController.

rails generate controller backend/resource

Normally inherited_resources advices you to inherit from InheritedResources::Base. In turn, InheritedResources::Base inherits from your own ApplicationController. But here we want to inherit from Backend::ApplicationController. Luckily, we can do that:

class Backend::ResourceController < Backend::ApplicationController
  inherit_resources
  respond_to :html
end

Now, if we want to create a CRUD controller, we can do that by inheriting from Backend::ResourceController.

rails generate controller backend/products

The controller code is now really simple:

class Backend::ProductsController < Backend::ResourceController
end

View inheritence

You're now free to make the views for every controller. This can be really boring, so it's a good thing that Rails 3.1 ships with a feature called 'view inheritence'. This means that if a view does not exist, it will go up the inheritence tree to find a view to render.

This means that we can create some simple views for our Backend::ResourceController and every resource controller can render those. Only if we want something different, we need to make a specific view. This means though that our views need to be agnostic about the model they're handling.

Inherited Resources provides a lot of helpers to make views agnostic of the model they are rendering. In the index action you have access to collection, in the other actions you can reference resource. There are also URL helpers like resource_path and collection_path.

But if we're agnostic of the model, we need to use inflection to see which fields we can render. We can ask the resource_class to see what attributes are defined. This will include automatic fields like id, updated_at and created_at too. But even without these fields, the list can become a bit too long for index pages.

In fact, I've found that the list of columns to display on the index page is something you want to customize just about every time. So let's build a simple view to be overriden in every controller. In app/views/backend/resource/index.html.haml, I put the following:

= render "index", attributes: attributes

And add to the Backend::ResourceHelper:

module Backend::ResourceHelper

  def attributes
    resource_class.attribute_names - %w(id created_at updated_at)
  end

end

This will render the index-partial with the first three attributes of the model. The index-partial then looks like this:

%table
  %thead
    %tr
      - attributes.each do |attr|
        %th= resource_class.human_attribute_name(attr)
      %th &nbsp;
  %tbody
    - collection.each do |resource|
      %tr[resource]
        - attributes.each do |attr|
          %td= resource.public_send(attr).to_s.truncate(20)
        %td
          = link_to 'show', resource_path(resource)
          |
          = link_to 'edit', edit_resource_path(resource)
          |
          = link_to 'destroy', resource_path(resource), method: :delete, confirm: "Are you sure?"

I can now easily override this per controller. If, for example, I wanted to only show the name column for products, I could create app/views/backend/products/index.html.haml with only this line:

= render "index", attributes: %w(name)

The show action probably wants to list everything, so it'll be something like this:

%dl
  - resource_class.attribute_names.each do |attr|
    %dt= resource_class.human_attribute_name(attr)
    %dd= resource.public_send(attr)

Simple Form

My favorite gem for creating forms is simple_form. It is really flexible. Install it, by adding it to your Gemfile and following the instructions in simple_form's README.

Our form partial in app/view/backend/resource/_form.html.haml will look something like:

= simple_form_for [ :backend, resource ] do |f|
  - attributes.each do |attr|
    = f.input attr
  = f.submit

And the new and edit pages will simply render this partial, and look similar to the index template:

%h2 Edit #{resource_class.model_name.human}

= render "form", attributes: attributes

Scopes & Pagination

For the index page, you'll need pagination too. My favorite choice is Kaminari in combination with has_scope:

gem 'kaminari'
gem 'has_scope', git: 'git://github.com/plataformatec/has_scope.git'

I'm using the version of has_scope directly, because it has a fixed a deprecation error.

Next, implement it by adding this line to Backend::ResourceController:

has_scope :page, default: 1

And put it in your view as well:

= paginate collection

Super easy! Don't forget to run the generators provided by kaminari to configure it further, if you want.

Responders

To get flash messages, we'll need to create a responder. The responders gem has a generator for this, which you should use. We'll need a small tweak to get it to work:

# app/controllers/backend/responder.rb
class Backend::Responder < ActionController::Responder

  include Responders::FlashResponder
  include Responders::HttpCacheResponder

  def initialize(*)
    super
    @flash_now = :on_failure
  end

end

I've renamed it to make it clearer that this is the responder for the backend only. For the reason of the initializer, see this pull request.

To get flash messages for validation errors, you need to customize the locale file that was generated, to include alert messages for create and update:

en:
  flash:
    actions:
      create:
        notice: '%{resource_name} was successfully created.'
        alert: '%{resource_name} could not be created.'
      update:
        notice: '%{resource_name} was successfully updated.'
        alert: '%{resource_name} could not be updated.'
      destroy:
        notice: '%{resource_name} was successfully destroyed.'
        alert: '%{resource_name} could not be destroyed.'

Next, you'll need to add it to Backend::ResourceController, because Inherited Resources will otherwise override it with its own responders.

# app/controllers/backend/resource_controller.rb
self.responder = Backend::Responder

By adding the HttpCacheResponder, the website speeds up quite a bit. It will set the appropriate headers, like ETags, based on the resource you're viewing. This means that if you're viewing a page for the second time, you don't have to render the view anymore and your Rails app only sends a 304 Not Modified to your browser. If you're viewing my example on Heroku, you can see Varnish adding another layer of caching on top of it, using those ETags. It makes your backend really snappy!

Authentication

Our backend needs to be private, so we'll need some form of authentication. In most cases (more than you'd think even) basic authentication will work. This can be easily done inside the Backend::ApplicationController, by adding one simple line:

http_basic_authenticate_with name: "frodo", password: "thering"

If you're opting for a more complex system, with users and so on, I would go for Devise. The powerful thing about Devise is that you can really easily create a multiple types of users who have nothing with each other in common. They each have their own tables and sign in pages.

You can configure Devise to use your own controllers instead of its own, so you can really separate the admins from the rest of the system.

To generate administrators:

rails generate devise admin

Then I prefer to place the views inside the backend namespace as well. This means that I need to recreate the controllers that Devise provides. This sounds like a lot of work, but it also means that I can really easily customize it. Rather than depending on the customization hooks that Devise provides (they are really good), I can override certain methods inside the controller. Devise's code is so nice, I can easily override just the functionality I like to make it work.

To do this, place the devise_for route inside your namespace:

namespace :backend do
  devise_for :admins, skip: :registrations
end

And create controllers like this:

class Backend::SessionsController < Devise::SessionsController
  layout "backend/sign_in"
end

As you can see, I can now easily define the backend layout inside the controller, without having to revert to a more complex hook method. Remember that, due to view inheritance, you don't need to install the views. The original views that Devise provides are in the inheritence tree, so they will be rendered if you don't provide your own.

And don't forget to activate the authentication to every controller in the backend, by adding to your Backend::ApplicationController:

before_filter :authenticate_admin!

Personally, I would try to avoid going with Devise and stick to basic authentication as long as possible. That's also one of the main reasons I don't tend to go with the of the shelf admin solutions. They require Devise. It adds a lot of overhead that I don't usually need. You might also want to check out the Railscast on creating your own authentication system.

Styling

Update I've switched to using the Twitter Bootstrap styling. It's a lot prettier and cleaner than wep-app-theme. Have a look at this commit to see what I've had to do. You can skip this chapter if you like now.

So, we now have a nice backend system in place. But it still looks ugly. Time to add some style! Let's get something of-the-shelf to get going. I like web-app-theme. This will make our backend look just like RailsAdmin.

The easiest way to get a theme, I found, is to just clone web-app-theme somewhere and copy over the files you need. Rails 3.1 has the asset pipeline, so this means we can have a nice spot to put them: in the vendor/assets directory.

mkdir -p vendor/assets/stylesheets/web-app-theme
cp ../web-app-theme/stylesheets/base.css vendor/assets/stylesheets/web-app-theme/
cp ../web-app-theme/stylesheets/themes/default/style.css vendor/assets/stylesheets/web-app-theme
cp -r ../web-app-theme/stylesheets/themes/default/images vendor/assets
cp -r ../web-app-theme/stylesheets/themes/default/fonts public

You need to change styles.css a bit to change the paths to images and any custom fonts. In Rails 3.1, assets aren't located in an images directory, but in /assets. Fonts are not served through Rails, so those should go inside the public directory. A quick find and replace should do the trick just fine.

Next up is the layout. You can install web-app-theme as a gem in your project and let it generate the layout for you. This is not necessary. You can find a layout inside lib/generators/web_app_theme/theme/templates/layout_admin.html.erb. Copy it to app/views/layouts/backend/application.html.erb. Remember, view inheritance will automatically pick it up for every controller in the backend. I'm a Haml user, so I converted it using html2haml:

gem install hpricot ruby_parser
mkdir -p app/views/layouts/backend
html2haml ../web-app-theme/lib/web_app_theme/theme/layout_admin.html.erb > app/views/layouts/backend/application.html.haml

You have some cleaning up to do. Or just grab the one I've already made! Afterwards, look at the classes web-app-theme uses and apply it to your views as well.

To include the stylesheets, I just include one inside the layout:

= stylesheet_link_tag "backend"

In app/assets/stylesheets/backend.css, I use the awesome power of Sprockets to include the rest:

/*
 *= require web-app-theme/base
 *= require web-app-theme/style
 *= require_tree ./backend
 */

It also includes every css file I have inside the app/assets/stylesheets/backend directory, which is where future customizations will go.

I also use the same structure for javascripts, creating the following app/assets/javascripts/backend.js:

//= require jquery
//= require jquery_ujs
//= require_tree ./backend

This styling would give you enough to start your backend. There are a ton of customizations you can do, but I'll leave that up to you.

More namespacing

Because of the way namespacing works, you can customize your models for use in the backend. This allows you to add methods for use in the backend only, without cluttering your classes in the rest of the application.

# app/models/product.rb
class Product < ActiveRecord::Base
  has_many :widgets
end

# app/models/backend/product.rb
class Backend::Product < ::Product

  def deactivate_all_widgets
    widgets.each(&:deactivate)
  end

end

Because Inherited Resources is in the Backend namespace, it will use this class instead of the normal one.

You can also make use of lexical scoping to implicitly reference the namespaced classes. This is quite subtle, so it's a bit error-prone.

In the following example, @product will be an instance of Backend::Product:

module Backend
  class ProductsController
    def new
      @product = Product.new
    end
  end
end

In the next example, @product will be an instance of ::Product, because only the class and module keywords will create the scope for you. You are scoped inside Backend::ProductsController, but not really inside Backend.

class Backend::ProductsController
  def new
    @product = Product.new
  end
end

Also, you don't need to explicitly tell the form that its resource needs to be scoped to the backend. There are a lot of tiny nuances that change when you go this route. So beware. Ruby's namespacing is powerful, but combined with Rails' naming conventions it can also be a bit confusing at times.

Conclusion

Making your own backend is really easy. Sure, there are out of the box gems that do all this for you and more. But if you want more control, yet still get some of the productivity, Rails 3.1 has all the tools to make it work. All the gems I've used fit nicely together. Sure, most of them are made by José Valim, or by his company Plataforma, but it shows how extensible the Rails platform really is.

Whether to choose for a custom solution or something more automatic is something you should decide on a per-project basis. If you're building it for non-technical customers, doing it yourself might be the better solution. But products like RailsAdmin give so much nice features, you'd better make sure.

comments powered byDisqus