Lightweight Web Apps: Getting Started With Sinatra

For many reasons, I’m a huge fan of Rails for building web software, and by implication, coding in Ruby. When you’ve spent a little time working with Ruby, it’s difficult to go back to more traditional languages such as PHP.

For less intense website or small applications, though, Rails can feel a bit heavy-handed. Whilst Rails 3 goes a long way to improving this, I’ve recently been trying out the lightweight ruby framework, Sinatra.

Sinatra is a barebones framework for getting web applications up and running quickly. The idea is that Sinatra gives you a base into which you pull together the things you need, effectively building your own mini-framework. The process is enhanced by the vast array of Ruby Gems that are available.

In this post, I’ll show how to build a simple Sinatra application configured with Bundler and running on Rack that lets you quickly start building lightweight web applications.

Creating Your Sinatra Project

Unlike Rails, Sinatra doesn’t come with generators, so you’ll need to create the project layout yourself. Whilst you can use your own folder layout, there are a few conventions for which Sinatra is configured. Something like this should be good to get started:

mkdir -p myproject myproject/views myproject/public myproject/public/javascript myproject/public/css myproject/public/images
touch myproject/app.rb myproject/config.ru myproject/Gemfile

Running these commands generates the following folder structure:

myproject/
  public/
    public/css
    public/images
    public/javascript
  views/
  tmp/
  app.rb
  Gemfile
  config.ru

If you’ve used Rails 3, or worked with the Bundler gem and Rack, most of this should look familar: Gemfile is used by Bundler to declare rubygems the project relies on, whilst config.ru is used by Rack to configure and run the application.

Bundling Gems

Next, edit the Gemfile to load Sinatra and any dependencies. My markup of choice for HAML for layout (HTML) and SASS for CSS stylesheets. Sinatra makes using HAML (or any other renderer, such as ERB) very simple:

# Gemfile
source :rubygems
 
gem "sinatra"
gem "haml"

To install the bundle, just run bundle install from within your project folder:

cd myproject
bundle install

Configure Sinatra to run on Rack

My development environment is setup to run Ruby projects on Rack using the Passenger gem. To configure Sinatra to run on Rack, our project just needs a config.ru file. This file tells Rack to load the any required gems and star your Sinatra application:

# Gemfile
require "rubygems"
require "bundler/setup"
require "sinatra"
require "haml"
require "app"
 
set :run, false
set :raise_errors, true
 
run Sinatra::Application

Build Your App

With the environment configured, it’s time to build a small app to test everything. Start by adding a root path to app.rb and a corresponding index.haml file under views/:

# app.rb
set :haml, :format => :html5
 
get "/" do
  haml :index
end
# views/index.haml
%html
  %head
    %title My Sinatra App
 
  %body
    %h1 Hello World
    %p Your app is up and running!

The get block routes any HTTP GET requests for the site’s root path (example.com/) and renders views/index.haml using the HAML parser.

Running Your Application

To see your application running, start Rack by calling rackup from your project directory.

cd myproject
rackup

Now navigate to localhost:9292 to see your Sinatra app working! All being well, you should be greeted with the following:

Summary

Initially I had trouble seeing how Sinatra would be useful given the existence of full-stack frameworks like Rails. However, as I’ve taken the time to learn Sinatra, I can see the benefits of such a lightweight framework when used for certain tasks.

Whilst it shouldn’t be thought of as a replacement for frameworks like Rails, Sinatra is another tool available to Ruby developers looking to quickly build and deploy small websites and applications.

When Should You Use Sinatra?

When you should use Sinatra over a larger framework like Rails is entirely dependent on the requirements of your site. Sinatra is capable of connecting with databases using gems like ActiveRecord or DataMapper in the same way Rails is. It’s feasible, then, to build large-scale web applications with Sinatra – but my personal preference would be to stick with Rails.

The rule of thumb I’m using at the moment is that if my app requires a database, I’ll build it in Rails, otherwise I’ll try Sinatra.

With that in mind, I’m now using Sinatra to build and run lightweight websites such as the recently launched portfolio of Mark Stocks. Mark’s site only requires simple server-side processing for handling contact forms – a full-blown Rails build seemed a be too much! Sinatra provided just the right base on which to build Mark’s site, whilst retaining the flexibility that comes with the Ruby ecosystem.

Next Steps

In the next tutorial, I’ll extend the basic framework we’ve started here to include of a number of useful Ruby gems built for Sinatra, and show how to use your own custom helper code. When it’s a little more polished, I’ll also publish my working Sinatra base application on github.

References

  1. The Sinatra Website
  2. The Official Sinatra Book
  3. Bare Sinatra App For Deploying To Passenger

Rails: Building Complex Search Filters with ActiveRecord and ez_where

With the release of Rails 3, the plugins and code described below no longer works. Read the updated post on building dynamic complex queries with composed scopes.

The code for this series of articles is also available:
git clone git://github.com/cblunt/blog-complex_search_filters_with_rails.git

This series of posts shows how to use the ez_where plugin to build a complex search filters into a Rails model. Last time we built a User model with a search method that allowed it to perform complex searches on terms and attributes. In this post, you’ll see how to tie everything together using a controller and search form.

Populating the Database

Before we get started, it would be handy to populate our database with a lot of data to see the search controller in action. To create fixture data, I use Ryan Bates’ Populator and Benjamin Curtis’ Faker gems. Once installed, a short rake task is all that’s needed to populate the database with some pseudo-random data. Alternatively, you can manually create some user records, e.g. by using script/console.

Lets add a populator rake task. First, make sure you have the faker and populator gems installed:

sudo gem install faker
sudo gem install populator

Then add a populator task to your project, in a new file lib/tasks/populate.rake:

# lib/tasks/populate.rake
namespace :db do
  namespace :populate do
    desc "Populate the users table with 1000 random users"
    task :users => :environment do
      require 'populator'
      require 'faker'
 
      # Destroy existing data
      User.destroy_all
 
      # Repopulate
      User.populate(1000) do |user|
        user.first_name = Faker::Name.first_name
        user.last_name = Faker::Name.last_name
        user.email_address = Faker::Internet.email
        user.status = rand(5) + 1
        user.admin = (rand(10) < 5 ? true : false)
      end
    end
  end
end

With that in place, run the rake task to populate your database with some users:

rake db:populate:users

(For a more thorough tutorial of Populator and Faker, see Ryan’s screencast).

Generating the Controller

With plenty of data to work with, it’s time to start building our users controller. In this example, I’ve only included the index and show actions. For a real-world app, you would want to include the remaining RESTful actions (edit, create, etc.).

ruby script/generate controller users index show

You also need to add a route to your config/routes.rb file so Rails can access the users controller RESTfully:

# config/routes.rb
map.resources, :users

With some routes set up, it’s a good idea to check that everything is connected properly. To do this, we’ll just ask the users controller index action to fetch a list of users. Remember that, without any parameters, our User.search method just wraps the User.find base method:

# app/controllers/users_controller.rb
def index
  @users = User.search(:all)
end

We’ll then output these user’s on the index page:

# app/views/users/index.html.erb
<h1>Users</h1>
 
<table>
  <tr>
    <th>Name</th>
    <th>Email Address</th>
    <th>Administrator</th>
    <th>Status</th>
  </tr>
  <% @users.each do |user| %>
    <tr>
      <td><%= h [user.first_name, user.last_name].join(' ') %> </td>
      <td><%= h user.email_address %></td>
      <td><%= user.admin? ? 'Yes' : 'No' %></td>
      <td><%= user.status %></td>
    </tr>
  <% end %>
</table>

A quick visit to http://localhost:3000/users will check everything’s hooked up (make sure your server is running; script/server will start the development server). If all has gone well, you will have a list of several hundred user names.

The Search Form

Next, we’ll add a text box so that we can search for users by terms. These terms will be passed on to the User.search method and used to filter users by first or last name, or email address. See part 1 for more information on how this works.

In your index view, add the following form just above the results table:

# app/views/users/index.html.erb
<% form_tag users_path, :method => :get do %>
  <%= text_field_tag "search[terms]", params[:search][:terms] %>
  <%= submit_tag "Search" %>
<% end %>
 
# ...

Notice that we need to make the form submit a GET request; if we used the default POST request, Rails’ RESTful routes would assume we wanted to create a new user. If you now reload the page in your browser, entering some search terms should direct you to a URL similar to:

http://localhost:3000/users?search[terms]=joe&amp;commit=Search

So now all we need to do is pass on those search parameters from our controller to User.search. In your controller, change the index action to:

# app/controllers/users_controller.rb
def index
  params[:search] ||= {}
 
  @users = User.search(:all, :filters => params[:search])
end

Now reload your page, and enter some search terms in the text field. You’ll notice that any terms you enter are matched against the users’ names or email addresses.

The Final Touches

With the general search terms working, it would be useful to narrow down our search according to some attribute filters. To do this, we’ll add a checkbox and dropdown menu that allows us to filter results by role and admin status. In index.html.erb, update your form with two new tags:

# app/views/users/index.html.erb
<% form_tag users_path, :method => :get do %>
  <%= text_field_tag "search[terms]", params[:search][:terms] %>
  <br />
 
  Only Admins <%= check_box_tag "search[admin]", true, params[:search][:admin] %>
  <br />
 
  Status: <%= select_tag "search[status]", options_for_select([["- All -", nil], ["Viewer", "1"], ["Member", "2"], ["Subscriber", "3"], ["Publisher", "4"], ["Editor", "5"]], params[:search][:status]) %>
  <br />
  <%= submit_tag "Search" %>
<% end %>

Your controller will now automatically pass on the :admin and :status values to User.search. However, if you try searching for administrators by checking the box, you’ll see that no users are returned!

The reason for this is how Rails passes parameters into your controller. The check_box_tag returns a string value depending on its status. Our User.search method expects an :admin filter to be a Boolean value, or nil. So all we need to do is tell Rails to convert the string “true” to a boolean true, or set the :admin filter to nil.

This is easy to solve by adding a single line in your controller to map the value of the checkbox to true or nil:

# app/controllers/users_controller.rb
def index
  params[:search] ||= {}
 
  # Ensure that params[:search][:admin] is either true or nil.
  params[:search][:admin] = (params[:search][:admin] == "true" ? true : nil)
 
  User.search(:all, :filters => params[:search])
end

Now when you reload the page, you’ll be able to search for users and drill down your search results using your new form!

Next Steps

I hope you’ve found this mini-tutorial series useful and interesting. It’s been a great learning experience for me in writing tutorials, and forcing me to refine my ruby coding. I’ve also found the search functionality useful in my own projects, and have decided to develop it into a generic ActiveRecord plugin. I’ll continue to post updates about progress on this blog, and on my github account.

In the near future, I’ll post some supplementary information on using the search methods with the will_paginate plugin, and how to AJAXify your seach forms.

In the meantime, if you have any comments or feedback, please let me know in the comments. Thanks!

Rails: Unit Testing Rails Single-Table Inheritance Models

I recently spent not an insignificant amount of time trying to get my unit tests to comply with Rails’ single-table inheritance. Single table inheritance allows you to map a hierarchy of classes to a single table in the database, so from one user table I can have an Employee and a Manager. They both inherit from a User model in Rails, and are stored in the users database table. A manager, though, might have more properties (fields) and methods than an Employee, which can be defined in manager.rb:

# user.rb
class User < ActiveRecord::Base
  validates_presence_of :name
end
 
# manager.rb
class Manager < User
  has_many :employees, :foreign_key => :user_id
 
  def grant_payrise(employee, amount = 100)
    employee.pay += amount
  end
end
 
# employee.rb
class Employee < User
  belongs_to :manager, :foreign_key => :user_id
 
  def ask_for_payrise
    self.manager.grant_payrise(self)
  end
end

In the code above, both employees and managers are stored in the users table from which they inherit. However, Rails will recognise which users are managers and which are employees from clever use of a reserved string field on the users table called type. The type field stores which class of user a record represents, and Rails’ finders will automatically recognise that field. This means your controller can call:

@employees = Employee.find(:all)

That code will only return users who are employees, and return them as Employee objects.

Unit Testing

My problem occurred when I tried to update my unit tests to cope with single-table inheritance. To keep things tidy, and separate my employee tests from my manager tests, I moved all my employee testing into test/units/employee_test.rb and my employee fixtures into test/units/employees.yml, and did the same for managers. When I ran the tests with a rake test:units, though, I was greeted with pages of SQL errors.

It turns out that you need to define all your fixtures to the root object (or direct descendent of ActiveRecord::Base), in my case the User class. So your tests for both employees and managers should still be placed in test/units/user_test.rb and the fixtures in test/units/fixtures.rb, e.g:

# users.yml
mary_manager:
  first_name: Joe
  last_name: Bloggs
  type: Manager     # Note the type field must match the ActiveRecord
 
joe_employee:
  first_name: Joe
  last_name: Bloggs
  type: Employee    # Note the type field must match the ActiveRecord class.
 
generic_user:
  first_name: John
  last_name: Stone
  type: User

In hindsight, this makes sense, but after some thorough research (read: Google), I couldn’t find much – if anything – on unit testing single-table inheritance models, so thought I’d post a note to help anyone else in this situation.

Welcoming 2009 with some Goals

A belated happy new year! I’ve been reading a lot of ‘lifehacker’ blogs recently – the original LifeHacker, ZenHabits and unclutterer.com being the main three – and am hoping that 2009 will be a good year to declutter (both digitally and in the real world!). I thought it best, then, to outline some goals at the start of the year and see where I get with them over the remaining 359 days.

Continue reading

Hosting Service Upgrade

I have now upgraded my hosting service and am in the process of migrating accounts for testing on the new system.

The upgrade means a lot more flexibility for my hosting packages, and revised package options will be made available as soon as everything is set up and running.

One of the main requirements I am hoping to get sorted as soon as possible is support for Ruby on Rails. I am also looking into the Mono project for running ASP.NET sites.