RubySource: Just Do It: Learn Sinatra, Part Two

In part one of this tutorial, we set Sinatra up and displayed some pages. Now the fun really starts – we will be using a database to store our tasks in.

In this tutorial we will be using SQLite for the local database. As its name suggests, this is a lightweight file-based database that doesn’t require any configuration. If you haven’t already got this installed, then see this page for some simple instructions.

In order to interact with the database, I will be using DataMapper. This is a ORM that works in a similar way to Active Record, but has a slightly different methodology and syntax.

In order to use DataMapper with SQLite, the following gems will need to be installed:

$ gem install data_mapper dm-sqlite-adapter sqlite3

Next, we need to make sure that the DataMapper gem is required at the top of the main.rb file:

require 'sinatra'
require 'datamapper'

Now we have to set up the database connection which is just one line of code to tell DataMapper that we will use a SQLite database called ‘development.db’ and it will be saved in the same folder as the app:

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

The Task Model

In order to save tasks to the database we will need to create a task class. The following code goes beneath the database connection in main.rb:

class Task
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String, :required = true
  property :completed_at, DateTime
end

The Task class is linked to DataMapper by the line include DataMapper::Resource which includes the DataMapper Resource module as a mixin – this is how you make any Ruby Class a DataMapper resouces. After this line, comes properties of the Task class. The :id property is of type Serial which gives each task an auto-incrementing identifier. After that, we only really need two more properties for our tasks – the name of the task (notice that I’ve made this a required property) and the completed date.

Adding Tasks

To get us started, we are going to use IRB to create a few tasks. Open up a command line prompt and type the following to access the app:

$ irb
ruby-1.9.2-p180 :002  require './main'
DataObjects::URI.new with arguments is deprecated, use a Hash of URI components (/home/daz/.rvm/gems/ruby-1.9.2-p180/gems/dm-do-adapter-1.1.0/lib/dm-do-adapter/adapter.rb:231:in `new')
= true

You can ignore the warning that appears when you require the file. This is due to a bug in DataMapper that is due to be fixed in the next release. Don’t worry, it doesn’t affect the app in any way.

Before we can add any tasks, we need to set up the Tasks table by migrating the model. This is done using the following command:

ruby-1.9.2-p180 :003  Task.auto_migrate!
= true

Now we are ready to add some tasks. This can be done in irb using the create method. Here are a few examples of adding a few tasks:

ruby-1.9.2-p180 :004  Task.create(name: "get the milk")
= #
ruby-1.9.2-p180 :005  Task.create(name: "order books") = #
ruby-1.9.2-p180 :006  Task.create(name: "pick up dry cleaning")
= #
ruby-1.9.2-p180 :007  Task.create(name: "phone plumber")
= #

Those tasks have now been saved to the database. If we want to see them, we just need to make a couple of small changes to the handler in main.rb that deals with the root url:

get '/' do
  @tasks = Task.all
  slim :index
end

Task.all gets all the tasks from the database and stores them as an array in the instance variable @tasks. Recall that instance variables are available to all the views, so I use this in the index.slim view to iterate through each task and display them as a list:

form action="/" method="POST"
  input type="text" name="task"
  input.button type="submit" value="New Task "
h2 My Tasks
ul.tasks
  - @tasks.each do |task|
    li.task= task.name

Start up the server by typing $ ruby main.rb at a command prompt, reload the page and you should see a nice list of tasks. This is all well and good, but we want to be able to add the tasks from the web page. This isn’t a problem – in fact, we already have a form to do it, we just need to plug it in to DataMapper so that the tasks are saved to the database.

In part one of the tutorial, we had the following handler that was used to process the POST request when the form was submitted:

post '/' do
  @task =  params[:task]
  slim :task
end

This just needs changing slightly to the following:

post '/' do
  Task.create  params[:task]
  redirect '/'
end

This creates a new task using the information stored in the params hash that was submitted in the form (in this case it is just the name of the task). It then redirects back to the home page (using a GET request this time), which should show the list of tasks including the new task. Give it a try in your browser.

Deleting Tasks

So far, so good, we can add tasks to our list, but how about deleting them? To do this we need to add a delete button to each task that will send a DELETE request to the server. While I’m at it, I’m going to separate the view code for tasks into its own separate file. Put the following code into a file saved as ‘task.slim’ in the views folder:

li.task id=task.id
  = task.name
  form.delete action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="times;"  title="Delete Task"

Now change index.slim to the following:

form action="/" method="POST"
  input type="text" name="task"
  input.button type="submit" value="New Task "
h2 My Tasks
ul.tasks
  - @tasks.each do |task|
    == slim :task, locals: { task: task }

There are a few things to notice here. Firstly, in the task view, we need to create a form for the delete button. This is so that we can include a hidden field with the attributes name="_method" value="DELETE". This is because most browsers don’t understand the HTTP verbs PUT and DELETE, only GET and POST. To get round this, Sinatra uses hidden form fields to ‘fake out’ the browser and process the request correctly. This means that when the delete form is submitted, it will be processed as a DELETE request.

In the index view, notice how I referenced the task view using the following line:

    == slim :task, locals: { task: task }

This is how Sinatra handles partials – you just call a template inline. In this case I need to tell it that the reference I make to task in the task.slim file is the same as the task that is being passed to the block.

All that is left to do is to write a handler for deleting tasks. You’ll notice that the url is given in the action attribute of the form as /task/#{task.id}. The corresponding handler looks like this:

delete '/task/:id' do
  Task.get(params[:id]).destroy
  redirect '/'
end

This code simply finds the task that was clicked on by using the id in the url and then uses the destroy method to delete it from the database. It then redirects to the root page where the remaining tasks will be listed.

Completing Tasks

To finish off, we just need to be able to complete the tasks. Remember when we created the Task class, we specified a completed_at property which tells us the date that the task was completed on. If the property is set to nil, then we can assume that the task has not been completed. Open up task.slim and change it to the following:

li.task
  = task.name
  form.update action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="PUT"
    -if task.completed_at.nil?
      input type="submit" value="  " title="Complete Task"
    -else
      input type="submit" value="✓" title="Uncomplete Task"
  form.delete action="/task/#{task.id}" method="POST"
    input type="hidden" name="_method" value="DELETE"
    input type="submit" value="times;" title="Delete Task"

We’ve now added a line to say when the task was completed, if it has been, and another form that contains a hidden form field specifying that this is a PUT request. This is because we are modifying the task’s properties (actually, a PATCH request should be used, but this isn’t supported until Sinatra 1.3). We also display a different button depending on whether the task has been completed or not. If a task has been completed then it shows a check mark, if it hasn’t been completed then it looks like an empty check box, so there is some visual feedback to show a task’s status.

Notice that the url given in the action attribute is exactly the same – this is because Sinatra will react differently to whichever HTTP verb is used. We now need to write a handler to deal with this request, so add this to the bottom of main.rb:

put '/task/:id' do
  task = Task.get params[:id]
  task.completed_at = task.completed_at.nil? ? Time.now : nil
  task.save
  redirect '/'
end

This finds the task based on the id in the url then sets the completed_at property to the current time if it hasn’t been set already, or reverts it back to nil if it has been set. In other words, the button will act as switch to toggle between the task being done and not done.

Restart the server and have a go at adding, deleting and completing tasks. We now have a fully functioning to do list – all in under 40 lines of code! It’s a bit rough around the edges and doesn’t look the best, but we will sort that out as well as adding multiple lists in part 3!