SuperDo - A Sinatra and DataMapper To Do List

It’s finally time for a new project (or song as I’m now calling them). In this tutorial I’m going to explain how to build a simple to do list app that uses a database to store the tasks in the list. In the tutorial I will cover the following:

  • Installing SQLite and Datamapper
  • Connecting to the Database
  • CRUD Actions in the database
  • RESTful urls in the browser

Before you start, make sure that you have installed Sinatra, Ruby and Ruby Gems.

I’ve called the app Supderdo and deployed it on Heroku so you can see the finished app running.

Note - I’ve made a few small changes to the code since I finished writing this so the code on this page is correct, but some of the tagged ‘code so far’ on github isn’t quite right. The final code is correct, it’s just that I can’t figure out how to go back and correct something once it has been tagged

Installing SQLite and Datamapper

In this tutorial I’m going to use SQLite for the database and Datamapper for the ORM to connect to the database. First let’s install SQLite. If you are using Ubuntu then the following command in the terminal should work:

sudo aptitude install sqlite3 libsqlite3-ruby

If you are using Mac OSX then I think it comes pre-installed (could somebody confirm this?) I don’t know how to install it on Windows, but I’m sure somebody (cough Lucas! cough) will know in the comments.

Then install the Datamapper gem:

sudo gem install data_mapper

You also need to install the sqlite adapter to allow Datamapper to be able to talk to SQLite:

sudo gem install dm-sqlite-adapter

Now you are all set to go and develop a dynamic web app.

Note If you are going to deploy this on Heroku, you will need to create a file called .gems in the root folder. This file needs to include a list of gems to be installed on Heroku:

sinatra
data_mapper
dm-postgres-adapter

Connecting to the Database

First thing we need to do is create a folder for our app, called todo. Inside this folder, create a file called main.rb and enter the following:

require 'rubygems'
require 'sinatra'
require 'data_mapper'

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

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

DataMapper.auto_upgrade!

There’s quite a lot going on here, so I’ll explain it all bit by bit.

The first bit just requires the necessary gems.

require 'rubygems'
require 'sinatra'
require 'data_mapper'

We need rubygems and sinatra for all sinatra applications and the ‘data_mapper’ gem includes all the necessary gems that make DataMapper tick.

The next bit is the connection string to the database:

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

This is a useful snippet to keep because it does a nice check to see if you are using Heroku. If you are then it connects to the Heroku online database and if not it will connect to your SQLite database, called development.db. If this doesn’t exist, it will create the file for you.

The next part of the code creates a Task class and the property definitions:

class Task
  include DataMapper::Resource

  property :id,           Serial
  property :name,         String
  property :completed_at, DateTime
end

You need the top line include DataMapper::Resource inside any class that will be using DataMapper. After that the properties are defined. The first one creates a unique id for each task. The Serial option after it means that it will auto-increment as each new task object is added to the database. The next property is :name which allows us to give each task a name. We need to tell DataMapper that this will be stored as a string. The next property is called :completed_at. This will be stored as a DateTime and tell us what time the task was marked as completed on. This will also tell us if the task has been completed or not. Any task with a value of nil, will not have been completed.

The last part of the code is the auto_upgrade method:

DataMapper.auto_upgrade!

This tells DataMapper to update the database with any changes that you make to the Task class. This means that we can add and remove new properties as we go along and DataMapper will make all the necessary changes to the database. This makes development really quick and speedy and avoids us having to get our hands dirty trying to change the database manually. The auto_upgrade command will preserve the data in the database. If you want to wipe away all the data then you can use the DataMapper::auto_migrate! method, but be careful as all the data will be lost for good.

Okay, so now the database is all set up, we can test it out. We don’t have a web interface to use yet, so we’re going to use the terminal. Fire up your terminal and navigate to the todo folder. Then type the following:

>$ irb -r main.rb

This will open up an irb shell, but because you added the -r main.rb on the end, all the code from that file is included in the irb session. This means that you are connected to the database, so we can have a go at creating, finding and deleting some tasks.

First of all, let’s have a look at all our tasks:

$irb(main): > Task.all
=> []

This shows an epmty array, that’s because there are no tasks saved in our database, so let’s create one:

$irb(main): > t = Task.new
=> #<Task @id=nil @name=nil @completed_at=nil>

$irb(main): > t.name = "Get milk"
=> "Get milk"

This task is currently residing in memory - we have to manually save it to the database:

irb(main):007:0> t.save
=> true

Let’s check it worked by searching for the first task:

irb(main):008:0> Task.first
=> #<Task @id=1 @name="Get milk" @completed_at=nil>

There is another way that tasks can be created, using the create method:

irb(main):009:0> Task.create(:name => "Get bananas")
=> #<Task @id=2 @name="Get bananas" @completed_at=nil>

Notice that you can feed parameters into the bracket after the method using Ruby’s hash assignment notation. You don’t need to save this manually as it is automatically saved. We can test this by asking to see all the tasks in our database:

irb(main):010:0> Task.all
=> [#<Task @id=1 @name="Get milk" @completed_at=nil>, #<Task @id=2 @name="Get bananas" @completed_at=nil>]

As you can see, both tasks are now shown in the array. It can get messy trying to pick our way through all the tasks in an array if there are lots of them, so instead we can just use the count method:

irb(main):011:0> Task.all.count
=> 2

This shows us that there are currently two tasks saved in the database.

That has covered creating and reading records in our database. Now let’s have a look at updating. Let’s say that we want to get half fat milk. First we need to find that record.

irb(main):012:0> t = Task.first(:name => "Get milk")
=> #<Task @id=1 @name="Get milk" @completed_at=nil>

This is an example of how to find records in the database using certain parameters. In this case we wanted the task with the name “Get milk”. This is now stored in the variable t. There are two ways to update this record. The first way is to manually change it and save it:

irb(main):014:0> t.name = "Get half fat milk"
=> "Get half fat milk"
irb(main):015:0> t.save
=> true
irb(main):016:0> t
=> #<Task @id=1 @name="Get half fat milk" @completed_at=nil>

The second way is to use the update method. Let’s say we actually wanted full fat milk.

 irb(main):017:0> t.update(:name => "Get full fat milk")
 => true
 irb(main):019:0> t
 => #<Task @id=1 @name="Get full fat milk" @completed_at=nil>

Now let’s look at the last of the CRUD actions - Deleting. Let’s say that we actually didn’t want any milk at all, so we want to delete that task. First of all we find the task, then we delete it using the destroy method:

irb(main):020:0> t = Task.get(1)
=> #<Task @id=1 @name="Get full fat milk" @completed_at=nil>
irb(main):022:0> t.destroy
=> true

Notice the get method that I used to find the task with an id of 1. This can only be used if you use the primary key to search for it. In this case the primary key is the id, which is 1 for this task. We will be using this quite often later to get the tasks from unique urls. We can check that it has been deleted by asking to see all the tasks again:

irb(main):023:0> Task.all
=> [#<Task @id=2 @name="Get bananas" @completed_at=nil>]

Now we can see that only our second task to “Get bananas” is still saved in the database.

This is all good fun and using the console to interact with the database provides you with a very good method for getting to know your way around and also for testing, but we are creating a web app, so we’ll need a front end for all these actions.

RESTful urls and CRUD Actions

The web interface that we will create to interact with our database will be based on the REST principle. This uses the http verbs POST,GET,PUT and DELETE. They tie very closely with the CRUD (Create, Read, Update, Delete) database operations. Basically each, task will have its own url that looks like /tasks/:id, where :id corresponds to the task’s unique id. For example, the task “Get bananas” that we created earlier would have the url, ‘/tasks/2’ because its id was 2. Whether we want to read,update or delete the task will depend on the http verb that the browser passes. In other words, the url will remain the same, but the action will be different. Each of these actions will be covered by a different handler in our Sinatra application.

Let’s start by creating a handler for viewing tasks. Open up main.rb and type the following:

require 'rubygems'
require 'sinatra'
require 'data_mapper'

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

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

# View a task
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

DataMapper.auto_upgrade!

The handler for showing a task is covered in this snippet of code:

# show task
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

What this does is get the task with the id that is entered into the url and assigns this to the instance variable @task. Remember that instance variable can be accessed in the views. We then tell Sinatra to display the task view using erb. But we don’t have a task view, so let’s create one now. In the views folder create a file called task.erb and enter the following code:

<h2><%= @task.name %></h2>

This is nothing fancy. Just a heading that will display the task’s name for now. While we’re at it, let’s create a layout for this app. Create a file called layout.erb in the ‘views’ folder and enter the following code:

<!DOCTYPE html>
<html lang="en">
<head>
<title>To Do List</title>
<meta charset=utf-8 />
</head>
<body>
<h1>To Do List</h1>

<%= yield %>

</body>
</html>

Again, nothing special. We’re just setting up some basic html and a main title heading that says To Do List. Let’s test it out. Save everything and start the server.

$> ruby main.rb

Now navigate in your browser to ‘http://localhost:4567/task/2’ and you should see the following:

Screenshot 1

You can see the lyrics so far on github

Creating New Tasks

Now let’s have a go at creating a task in the browser using a form. The usual way is to split the create action into two handlers. One is called ‘new’ and displays the form and the other is the create handler that actually creates the task (usually in the background).

Let’s start off with the new handler and form. The handler is easy, all we want to do is to display the form when the user goes to ‘/task/new’, here’s the code that will do this (Note that it important to put this code before the show handler):

# new task
get '/task/new' do
  erb :new
end

This just tells Sinatra to display the erb page called ‘new’. Let’s create that now. Go into the views folder and create a file called ‘new.erb’. Open it up and enter the following code:

<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Add Task!"/>
</form>

This is a simple form that allows the user to create a new task and gives it the value of ‘name’ (this corresponds to the value given to it in the database, which isn’t necessary, but can make things much easier).

Submitting this form posts the data to a page called ‘task/create’. This doesn’t exist yet, so we will create a handler for it. This is a bit more complicated than the last one, but not too much:

# create new task   
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/'+task.id.to_s  
  else
    status 412
    redirect '/tasks'   
  end
end

Let’s have a look at what is happening in the code here. First of all it checks to see if there was a POST request, as the data has to be submitted from a form. Then it creates a new task and gives it the name stored in the params hash as :name (this comes from the form). We then check to see if the task is saved correctly. If it is, then we give it a status of 201, which is the http code to say that something was created, and redirect the user to the task’s url by appending its id on the end of the string ‘/task/’ (this uses the show handler we created before). If the task is not saved, then a status of 412 is returned which tells the browser that some precondition (such as validations) wasn’t met. The user is then redirected to the index page ‘/tasks’ (which we haven’t actually created yet, but don’t worry, it’s the next thing we’ll be doing).

Let’s test it out and add a task in the browser. Go to ‘http://localhost/task/new’ and you should see a form like the one in the screenshot below:

Screenshot 2

Go ahead and whatever task you want and after submitting the form you should see a page showing the name of your new task.

After all this your main.rb file should look like this:

require 'rubygems'
require 'sinatra'
require 'data_mapper'

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

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

# new task
get '/task/new' do
  erb :new
end

# create new task   
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/'+task.id.to_s 
  else
    status 412
    redirect '/tasks'   
  end
end

# show task
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

DataMapper.auto_upgrade!

You can see the lyrics so far on github

List All Tasks

Now we can create new tasks and view them all from within our browser. What we want to do now is show all of the tasks. The handler looks like this:

# list all tasks
get '/tasks' do
  @tasks = Task.all
  erb :index
end

This simply gets all the tasks from the database using the all method and stores them as an array in an instance variable called @tasks that will be accessible in the view. It then displays the index.erb file, which we’re going to create now (make sure you save it in the views folder):

<h2>Tasks:</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
<li <%= "class=\"completed\"" if task.completed_at %>>
<a href="/task/<%=task.id%>"><%= task.name %></a>
</li>
<% end %>
</ul>
<% else %>
<p>No Tasks!</p>
<% end %>

This starts off by checking if the task array is empty. If it isn’t then it proceeds to iterate through this array, creating an unordered list. It also checks to see if the task has been completed or not and adds a class of ‘completed’ if it has been. This will be useful when it comes to styling later. If the @tasks array is empty then a message is displayed saying that there are no tasks. If you go to ‘http://localhost/tasks’ in your browser you should see the following:

Screenshot 3

Editing Tasks

Only a couple more actions needed - edit and delete. Let’s start off by allowing people to edit tasks. Like new and create, the edit handler is paired up with the update handler. The edit handler simply displays a form so the user can enter the new information and submit it. The update handler actually updates the values in the database. They can both be seen in the code below:

# edit task
get '/task/:id/edit' do
  @task = Task.get(params[:id])
  erb :edit
end

# update task
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ?  Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/task/'+task.id.to_s
  else
    status 412
    redirect '/tasks'   
  end
end

We also need the edit form, so create a file called ‘edit.erb’ in the views folder and enter the following code:

<form action="/task/<%= @task.id %>" method="post">
<input name="_method" type="hidden" value="put" />
<input type="text" name="name" id="name" value="<%= @task.name %>">
<input id="completed" name="completed" type="checkbox" value="done" <%= @task.completed_at ? "checked" : "" %>/>
<input id="task_submit" name="commit" type="submit" value="Update" />
</form>

There’s quite a bit going on here. First of all the edit handler basically just displays a form if the user enters the url ‘task/2/edit’. The form is very similar to the new task form, but has a few differences. First of all the url that it posts to is ‘/task/2’. This is the same as the url to show a task, but the next line <input name="_method" type="hidden" value="put" /> is a hidden field that tells the browser that this is a PUT request. This is required because at the moment virtually all browsers don’t understand PUT requests, so Sinatra allows this ‘faking it’ method. This involves sending the request as a POST request but with a hidden field with a value of “put”. Sinatra sorts this out and routes this request to the relevant handler that uses put (in this case it is the update handler above). After this there is the name field that is pre-populated with the task’s current name, a check box that let’s you tick off if the task has been completed or not and the submit button.

The update handler is similar to the create handler. First of all it goes to the database and finds the task using the id stored in the params hash (it gets this value from the url, not the form). It then checks to see if the check box was checked. If it was then it sets the completed_at property to the current time, effectively marking the task as completed. If not, then it just stores the value as nil. It then updates the name field and attempts to save the task in the same way the create handler did.

Let’s test this out. Say that I didn’t actually want to go running but wanted to go cycling instead, I would click on the ‘go running’ link on the tasks page, then add ‘/edit’ onto the end of the url (I know this is awful usability, but don’t worry, it will get sorted out later). You should see a screen like the one below and be able to change the name of the task and click update to see the results.

Screenshot 4

You can see the lyrics so far on github

Deleting Tasks

The last thing we need to implement is deleting tasks. This is going to be done in two steps. First of all, let’s put a delete link on the edit page. Open up edit.erb and change it to the following:

<form action="/task/<%= @task.id %>" method="post">
<input name="_method" type="hidden" value="put" />
<input id="completed" name="completed" type="checkbox" value="done" <%= @task.completed_at ? "checked" : "" %>/>
<input id="task_submit" name="commit" type="submit" value="Update" />
</form>

<a href="/task/<%= @task.id %>/delete">Delete this task</a>

This last line adds a link to ‘task/:id/delete’ that will show a page asking if you are sure you want to delete it. Now let’s add a handler for the delete confirmation page in main.rb:

# delete confirmation
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

All this handler does is find the task and store it in an instance variable called @task. We need to use an instance variable (starting with @) in this case because we will be referring to it in the confirmation page. Now we need to make that confirmation page, so create a new file called delete.erb and save it in the ‘views’ folder. Open it up and enter the following code:

<h2><%= @task.name %><h2>
<h3>Are you sure you want to delete this task?<h3>
<form action="/task/<%= @task.id %>" method="post">
<input type="hidden" name="_method" value="delete" />
<input type="submit" value="Delete">
 or <a href="/tasks">Cancel</a>
</form>

This view is very similar to the form for editing the task. Notice again that we have to use a hidden field to fake out the DELETE method because most browsers will not understand it. We’ve also included an option to cancel the delete and return to the main tasks page.

Next we create the code to actually delete the task. Add the following handler to the main.rb file:

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

Let’s test this out by deleting the ‘Go to gym’ task (Not necessary if I’ve already been cycling anyway…). Click on the relevant task and then on the ‘delete this task link’. You should get the delete confirmation page like in the screenshot below:

Screenshot 5

The last two sections are a good example of how REST works. The show, update and delete handlers all have exactly the same url (for example ‘task/2’) and relate to the same object (in this case the task with an id of 2). But they each do very different things and are accessed depending on the verb used (GET, PUT and DELETE respectively).

The entire main.rb file should now look like this:

require 'rubygems'
require 'sinatra'
require 'data_mapper'

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

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

# list all tasks
get '/tasks' do
  @tasks = Task.all
  erb :index
end

# show task
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

# new task
get '/task/new' do
  erb :new
end

# create new task   
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/'+task.id.to_s   
  else
    status 412
    redirect '/tasks'   
  end
end

# edit task
get '/task/:id/edit' do
  @task = Task.get(params[:id])
  erb :edit
end

# update task
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ?  Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/task/'+task.id.to_s
  else
    status 412
    redirect '/tasks'   
  end
end

# delete confirmation
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

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

DataMapper.auto_upgrade!

This now includes the 7 standard REST handlers - index, show, new, create, edit, update, delete as well as an extra handler for the delete confirmation page.

You can see the lyrics so far on github

Usability Improvements

The application is now complete, but I think we can improve on it in a number of areas. As I mentioned above, the code currently uses the standard Rails conventions for RESTful urls. One of the nice things about Sinatra is you can do things your way, so I’m going to change a few of these urls.

First of all, I’d like the home page to be the place that lists all the tasks rather than the url ‘/tasks’. This simply requires changing the handler to reflect this:

# list all tasks
get '/' do
  @tasks = Task.all
  erb :index
end

I’d also like the form to add a new task to be on the home page. So copy the code from ‘new.erb’ and paste it into ‘index.erb’ (these are in the ‘views’ folder). The index.erb file should now look like this:

<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Add Task!"/>
</form>

<h2>Tasks:</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
<li <%= "class=\"completed\"" if task.completed_at %>>
<a href="/task/<%=task.id%>"><%= task.name %></a>
</li>
<% end %>
</ul>
<% else %>
<p>No Tasks!</p>
<% end %>

You can now delete the ‘new.erb’ file as well as the new handler in ‘main.rb’, but we still need to keep the create handler. I’m also going to delete the show handler and ‘show.erb’ file because all it does is show a task’s name which can be seen from the list on the home page anyway. This frees up the url ‘task/:id’ to be used for the edit pages instead, so change the edit handler to the following:

# edit task
get '/task/:id' do
  erb :edit
end

This neatens things up a little bit. There are also a few redirects that currently point to urls that don’t exist any more, so they need to be changed, usually to the root url (’/’). Your main.rb file should now look a bit leaner, like so:

require 'rubygems'
require 'sinatra'
require 'data_mapper'

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

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

# list all tasks
get '/' do
  @tasks = Task.all
  erb :index
end

# create new task   
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/'  
  else
    status 412
    redirect '/'   
  end
end

 # edit task
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :edit
end

# update task
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ?  Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/'
  else
    status 412
    redirect '/'   
  end
end

# delete confirmation
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

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

I’m also going to add a link back to the homepage that lists all the tasks by clicking on the main title. This is done by editing the ‘layout.erb’ file in the views folder:

<head>
<title>SuperDo</title>
<meta charset=utf-8 />
</head>
<body>
<h1><a href="/">My Super To Do List</a></h1>

<%= yield %>

</body>
</html>

This should now be much easier to use and behave a lot more snappier to use.

Screenshot 6

You can see the lyrics so far on github

Under the Hood Improvements

Next I’m going to add a few methods to tidy the code up a little bit. First of all I am going to create a couple of methods for dealing with completed tasks. Because this is a method for tasks, it needs to go inside the class definition (at the top of the main.rb file):

class Task
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String
  property :completed_at, DateTime

  def completed?
    true if completed_at
  end

  def self.completed
    all(:completed_at.not => nil)
  end

end

The first method is an instance method that is passed to individual tasks. It uses the fact that if there is a date set for the completed_at property, then the task must have been completed. It will return true if it is complete are and false if it’s not.

The second method is a class method and affects all the tasks. It can be used to filter your searches using DataMapper, for example the method Task.completed will find all the tasks that have been completed. The nice thing about these methods are that they can be chained together to produce fine-grained searches. For example, if there was another class method called important that returned all the Tasks that were important (not really possible at the moment, but maybe we will add this later!) then Task.important.completed would return all the tasks that were important and had been completed.

Another method I’d like to include is one that will produce a link to the task. You might recall that there was some fairly horrible looking code to produce this in the index list earlier:

<a href="/task/<%=task.id%>"><%= task.name %></a>

This would be much nicer if we could hide it in an instance method, like so:

class Task
  include DataMapper::Resource
  property :id,           Serial
  property :name,         String
  property :completed_at, DateTime

  def completed?
    true if completed_at
  end

  def self.completed
    all(:completed_at.not => nil)
  end

def link
  "<a href=\"task/#{self.id}\">#{self.name}</a>"   
end

end

The link method simply returns a string with a link to the task’s edit page and it’s name. This is done by using Ruby interpolated string notation where you can put code that needs to be evaluated inside #{}. Notice that I also used the key word self to refer to the task that the method is being applied to. This means that we can now clean up some of the view code. Open up ‘index.erb’ and make the following changes:

<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Add Task!"/>
</form>

<h2>Tasks:</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
<li <%= "class=\"completed\"" if task.completed? %>>
<%= task.link %>
</li>
<% end %>
</ul>
<% else %>
<p>No Tasks!</p>
<% end %>

This is now much more readable and will make the code easier to maintain. One last job is to style the tasks that have been completed. They already have a class of ‘completed’ if they have been (check the source to see), so styling them is as easy as adding a few lines to the head of ‘layout.erb’:

<head>
<title>SuperDo</title>
<meta charset=utf-8 />
<style>
.completed{text-decoration:line-through;}
</style>
</head>
<body>
<h1><a href="/">My Super To Do List</a></h1>

<%= yield %>

</body>
</html>

This finishes the project off nicely. You can now edit some tasks and tick that they have been completed and you can see this visually on the list of all tasks. The final app should look something like the screenshot below:

Screenshot 7

You can see the lyrics so far on github

Wrapping Up

This is a very simple database app, but it is a good start. It certainly doesn’t live up to its name at the moment as there is nothing particularly ‘super’ about it, but there are loads of places we can now go with this. For example you could try adding different list that have many tasks (read up on DataMapper associations), you could add some Javascript and Ajax to improve usability or you could add a priority setting and put the highest priority tasks at the top of the list. Have a go at experimenting with the code and post any ideas in the comments.

If you have any suggestions about how the code be improved, or any questions about how it works, please leave a comment below.

You can find the full lyrics on github.

blog comments powered by Disqus