Kim Rudolph

Rails Single Page Web Application

The goal of this example is to build a single page application using the AJ-part of AJAX and the web framework Ruby on Rails.

The result should be a small application providing the functionality to track time on tasks grouped by projects. It should create and delete tasks and projects without a full page reload. Tracked time per task is definied with one or many logs having a start and stoptime. Also there have to be those web-2.0-ish flashing elements when entities are being created, updated or deleted.

Screenshot of Trackapp

Code comments are stripped out intentionally to shrink the size of the code examples and make them more readable. Also user authorization and authentication are missing for the same reason. The tutorial guides through the minimal steps setting up such an application and does not feature a path to a secure and production ready application.

Have a look at the following resources, if you want more detailed and feature rich examples:

Installing Ruby

There are a lot of tutorials how to install ruby so this is a short version using rvm.

$ curl -L https://get.rvm.io | shell -s stable --ruby

Be aware that rails 4.2 needs at least ruby 1.9.3. The current stable version should be something like

$ ruby -v
ruby 2.1.2p95 (2014-05-08)

To prevent any failures during the installation of gems, the requirements defined in the ruby part in

$ rvm requirements

should be installed.

Installing Ruby on Rails

That is as simple as installing the latest rails gem.

$ gem install rails

Creating the Application Skeleton

One of the hardest tasks is to settle with a name for a project. As this application should track time, it’s called trackapp. Ruby on Rails will build an application skeleton by default. That application and folder structure is the same for every Ruby on Rails application. The controllers are always in the app/controllers folder, the models in app/models and the views in app/views etc.

$ rails new trackapp
create
create  README.rdoc
create  Rakefile
create  config.ru
create  .gitignore
create  Gemfile
create  app
...
create  vendor/assets/stylesheets
create  vendor/assets/stylesheets/.keep
run  bundle install

There is a requirement for an ExecJs runtime to compile to JavaScript. therubyracer is suggested, but has to be uncommented manually in the Gemfile.

Gemfile

...
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
gem 'therubyracer', platforms: :ruby
...

To resolve the depedencies, the bundler gem is used.

$ bundle install

Database Schema

Database changes, like creating new tables, are defined in migration files. A migration is needed to initially setup the database tables for the projects, tasks and logs.

$ rails generate migration SetupProjectsTasksLogs

The create_table block contains the fields to add to the defined table. t.references :project is a shortcut for the field t.integer :project_id.

db/migrate/x_setup_projects_tasks_logs.rb

class SetupProjectsTasksLogs < ActiveRecord::Migration
  def change

    create_table :projects do |t|
      t.string :name
    end

    create_table :tasks do |t|
      t.string :name
      t.boolean :done
      t.references :project, index: true
      t.timestamps
    end

    create_table :logs do |t|
      t.references :task, index: true
      t.timestamp :start
      t.timestamp :stop
    end

  end
end

To perform the changes, the database needs to be created and then migrated to the SetupProjectsTasksLogs state. A local sqlite database is used by default.

$ rake db:create db:migrate

Models

There are serveral ways to scaffold any models, views, controllers, tests etc., but that often results in a lot of empty files. Adding the files manually is complicated at first, because the strict naming guidelines have to be followed. The advantage is a clean setup of only those files that are needed.

The model files project.rb, task.rb and log.rb in app/models describe each entity and the dependencies between the entities.

A project has many tasks and also many logs. The logs are not referenced directly, but the :through => :tasks enables the access like Project.logs using a join on the tasks table.

Entity validations are definied with the validates method. In this case, only the name has to be checked. It is not allowed to create new projects with an empty name.

The :dependent => :destroy part defines how to keep data integrity if a referenced object is deleted. A deleted project results in the deletion of all its tasks and logs as they are not needed anymore. That garantees that no unlinked tasks or logs remain in the database.

Both, projects and tasks, provide a logged method to sum up the time spend on a project or task.

app/models/project.rb

class Project < ActiveRecord::Base

  has_many :tasks, dependent: :destroy
  has_many :logs, through: :tasks

  validates :name, presence: { message: 'Please provide a name' }

  def logged
    logs.map(&:duration).sum
  end

end

A task belongs to a project and also has a default scope, which orders the tasks by its status and the time they were created if not defined otherwise. That keeps the recent tasks on top of the tasks list. Tasks must have a linked project defined by the validates presence of method.

app/models/task.rb

class Task < ActiveRecord::Base

  belongs_to :project
  has_many :logs, dependent: :destroy

  validates :name, presence: { message: 'Please provide a name' }
  validates_presence_of :project

  def work?
    !logs.empty? && !logs.first.stop
  end

  def logged
    logs.map(&:duration).sum
  end

end

A log uses the methods mentioned above and also calculates the seconds between the start and stop time.

app/models/log.rb

class Log < ActiveRecord::Base

  belongs_to :task

  default_scope { order(:stop) }

  validates_presence_of :task

  def duration
    ((stop? ? stop.to_time : Time.now) - start.to_time).to_i
  end

end

Test Fixtures

The test classes need real data to perform the tests on. To simplify that, the test objects are defined as fixtures in the test/fixtures folder. The test data should represent the different possibilites of the object stages including all dependencies. The two tracking cases are

test/fixtures/projects.rb

tree:
  id: 1
  name: Cutting a tree
car:
  id: 2
  name: Washing the car

test/fixtures/tasks.rb

finished:
  id: 1
  name: Easy task
  done: true
  project_id: 1
unfinished:
  id: 2
  name: Hard task
  project_id: 2

test/fixtures/logs.rb

hour:
  task_id: 1
  start: 2000-01-01 12:00:00
  stop: 2000-01-01 13:00:00
long:
  task_id: 1
  start: 2000-01-01 11:00:00
  stop: 2000-01-01 12:34:56
minutes:
  task_id: 2
  start: 2000-01-01 12:00:00
  stop: 2000-01-01 12:11:10
running:
  task_id: 2
  start: 2000-01-01 12:00:00

Model Tests

Test for the models should be created in the test/models folder. At least there should be a simple test for each created model method.

The ProjectTest checks

test/models/project_test.rb

require 'test_helper'

class ProjectTest < ActiveSupport::TestCase

  test 'should not save without a name' do
    project = projects(:tree)
    project.name = ''
    assert !project.valid?, 'Saves without a name'
    assert_equal 'Please provide a name', project.errors[:name].first, 'Does not throw an error message'
  end

  test 'should destroy all dependend tasks and logs' do
    project = projects(:tree)
    assert_equal 1, project.tasks.size
    assert_equal 2, project.tasks.first.logs.size
    project.destroy
    assert_raises ActiveRecord::RecordNotFound do
      tasks(:finished)
    end
    assert_raises ActiveRecord::RecordNotFound do
      logs(:long)
    end
    assert_raises ActiveRecord::RecordNotFound do
      logs(:hour)
    end
  end

  test 'should calculate the logged time correctly' do
    assert_equal 9296, projects(:tree).logged, 'Does not calculate the logged time correctly'
  end

end

The TaskTest checks

test/models/task_test.rb

require 'test_helper'

class TaskTest < ActiveSupport::TestCase

  test 'should not save without a project' do
    task = tasks(:finished)
    task.project = nil
    assert !task.valid?, 'Saves without a project'
  end

  test 'should not save without a name' do
    task = tasks(:finished)
    task.name = ''
    assert !task.valid?, 'Saves without a name'
    assert_equal 'Please provide a name', task.errors[:name].first, 'Does not throw an error message'
  end

  test 'should calculate the logged time correctly' do
    assert_equal 9296, tasks(:finished).logged, 'Does not calculate the logged time correctly'
  end

  test 'should return the correct work status' do
    assert !tasks(:finished).work?, 'Does not return work status'
    assert tasks(:unfinished).work?, 'Does not return work status'
  end

end

The LogTest checks that it is

test/models/log_test.rb

require 'test_helper'

class LogTest < ActiveSupport::TestCase

  test 'should save without a stop time' do
    assert logs(:running).valid?, 'Does not save without a stop time'
  end

  test 'should not save without a task' do
    log = logs(:hour)
    log.task = nil
    assert !log.valid?, 'Saves without a task'
  end

  test 'should return the correct duration' do
    assert_equal 3600, logs(:hour).duration, 'Does not calculate duration right'
    assert_equal 670, logs(:minutes).duration, 'Does not calculate duration right'
  end

end

Now the test execution should create 11 small and happy dots, one for each non failing test case.

$ rake test:models

Routing

The tracking application should respond to several request URLs and different HTTP verbs.

Resources helpers create the default CRUD routings: index, new, create, show, edit, update and destroy. As the goal is a single page application, rendering new pages for entity updates (new, edit) is not needed. While projects can be switched with the show action and the initial page loads with the index action, those are not needed for tasks. But tasks need additional routings for starting and stopping logs and also marking a task as finished. Those are defined as resource members.

root to: defines the action, which is executed when a request points to /.

config/routes.rb

Trackapp::Application.routes.draw do

  resources :projects, only: [:index, :show, :create, :destroy]

  resources :tasks, only: [:create, :destroy] do
    member do
      get :start, :stop, :finish
    end
  end

  root to: 'projects#index'

end

The generated and valid routes can be checked with the rake routes command.

$ rake routes
     Prefix Verb   URI Pattern                 Controller#Action
   projects GET    /projects(.:format)         projects#index
            POST   /projects(.:format)         projects#create
    project GET    /projects/:id(.:format)     projects#show
            DELETE /projects/:id(.:format)     projects#destroy
 start_task GET    /tasks/:id/start(.:format)  tasks#start
  stop_task GET    /tasks/:id/stop(.:format)   tasks#stop
finish_task GET    /tasks/:id/finish(.:format) tasks#finish
      tasks POST   /tasks(.:format)            tasks#create
       task DELETE /tasks/:id(.:format)        tasks#destroy
       root GET    /                           projects#index

Controllers

Controllers must match the routings listed above with a method for each action.

The usage of Project.includes(task: :logs) enables fetching all tasks and logs needed for the initial page load. That prevents querying each task and each log individually while rendering the view.

Non controller action matching view delegations have to be specified. In this case the render :show on the destroy action will point to the projects show view. An additional view is not neccessary, because the deletion of projects will also result in rendering another project.

app/controllers/projects_controller.rb

class ProjectsController < ApplicationController

  def index
    @projects = Project.all
    @project = Project.includes(tasks: :logs).first
    @project ||= Project.create(name: 'Demo Project')
  end

  def show
    @project = Project.includes(tasks: :logs).find(params[:id])
  end

  def create
    @project = Project.create(name: params[:name])
    @projects = Project.all
  end

  def destroy
    @deleted = Project.find(params[:id]).destroy
    @project = Project.includes(tasks: :logs).first
    render :show
  end

end

The start, stop and finish actions result in a refresh of the single task view and therefore share one view.

app/controllers/tasks_controller.rb

class TasksController < ApplicationController

  def create
    @task = Project.find(params[:project_id]).tasks.create(name: params[:name])
  end

  def destroy
    @task = Task.find(params[:id])
    @task.destroy
  end

  def finish
    @task = Task.find(params[:id])
    @task.update(done: true)
    render :refresh
  end

  def start
    @task = Task.find(params[:id])
    @log = Log.create(task: @task, start: Time.now)
    render :refresh
  end

  def stop
    @task = Task.find(params[:id])
    @log = @task.logs.first.update(stop: Time.now)
    render :refresh
  end

end

Controller tests will be defined after the views.

Styling Assets

Moving on from the known plain HTML/Erb template language and css format, there are projects like Haml, Slim and Sass as replacements, advertising themselves as more lightweight and therefore easier to work with. Haml and Slim strip as many unneeded tags and identifiers as possible from templates. The Sass projects supports the .scss format as a superset of css and the more radical sass format, removing braces and semicolons completely. Haml, Slim and Sass rely heavily on correct indentation.

As there are many choices, this tutorial will stick to slim and sass, because both support the most minimal syntax. Rails recognizes the .slim file extension as handlers when it is looking for a fitting view to render. So the view files have to be named like task.html.slim.

The slim-rails gem is needed as an asset dependency in the Gemfile.

Gemfile

...
gem 'slim-rails'
...

bundle install installs that dependency.

JavaScript and stylesheets are located in the assets folder. This application should use Sass, so the default css behaviour has to be modified. There are small steps needed to teach the application to work with Sass.

Rename the application.css located in assets/stylesheets to application.sass. Then remove the line *= require_tree . which would enable searching for any stylesheets automatically. Using Sass, importing the needed files gives more control over which files should be added and allows a custom order of files included. A disadvantage is the manual work to add each file individually. In this case, the files used are:

app/assets/css/application.sass

/*
 ...
 *= require_self
 */
@import 'colors'
@import 'logs'
@import 'tasks'
@import 'projects'

E.g. the @import 'colors' points to the colors.sass file in the same directory. The actual stylesheets are too large to show them here, but they can be viewed at the trackapp repository stylesheets folder.

Dynamic Assets

Rails supports CoffeeScript by default. CoffeeScript does to JavaScript what Slim does to the HTML/Erb templates. It provides a language that compiles to plain JavaScript. As it makes writing JavaScript a lot easier, the JavaScript templates are written in CoffeeScript.

As mentioned, the application needs those web-2.0-ish flashing elements. The needed logic is implemented as a notice()-method, which is extracted to the functions.js.coffee file.

app/assets/javascript/functions.js.coffee

@notice = (element, color) ->
  element.animate {backgroundColor: color}, 800
  element.animate {backgroundColor: "#fff"}, 800

A toggable-method is also added to register an event to a task link to toggle the container showing the logs.

app/assets/javascript/functions.js.coffee

@toggable = (parent) ->
  parent.find('.show-logs').on 'click', ->
    $(this).parents('.task:first').find('.task-logs').toggle 'slow'

All tasks logs should be toggable on first load.

$ ->
  for task in $('.task')
    toggable $(task)

Adding a third party resource

The application definitely needs color highlighting on elements when they are being updated. The jQuery animate method needs the color plugin to support color changes as defined in the notice() method.

As it is a third party library, which is not available as a gem, the dependency has to be added manually. The file belongs in the vendor path as vendor/assets/javascripts/jquery-color-2.1.2.min.js. It also has to be referenced in the app/assets/application.js file. Otherwise it would not be recognized as a needed asset.

app/assets/javascript/application.js

...
//= require turbolinks
//= require jquery.color-2.1.2.min
...

Helping Hands with the View

Following the DRY principle, often needed view logic should be extracted in a helper class. That helps keeping the view code smaller and also provides a single place if the method logic needs an update.

The application should print logged time in a familiar clock format like 12:05:17. That logic is extracted at app/helpers/logs_helper.rb using the format method.

app/helpers/logs_helper.rb

module LogsHelper

  def format_logged(total)
    format("%02d:%02d:%02d", total/(60 * 60), (total/60) % 60, total % 60)
  end

end

The corresponding test in test/helpers/logs_helper_test.rb should check if the calculation works.

test/helpers/logs_helper_test.rb

require 'test_helper'

class LogsHelperTest < ActionView::TestCase

  test 'should format logged time' do
    assert_equal '01:00:00', format_logged(3600), 'Does not format logged time'
    assert_equal '34:17:36', format_logged(123456), 'Does not format logged time'
  end

end

Now the test execution should create 1 small and happy dot.

$ rake test:helpers

Views

Coming from the backend, the visual part is defined as views. Defining = yield shows the location where the content will be included in the main application template. The templating engine Slim provides serveral methods and references to keep the promise of writing only the neccessary code, e.g. the doctype helper.

app/views/layouts/application.html.slim

doctype html
html
  head
    title Trackapp
    = stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true
    = javascript_include_tag "application", "data-turbolinks-track" => true
    = csrf_meta_tags

  body
    = yield

Partials describe the display of a single entity. A partial view is prefixed with an underscore and referenced in other views by an include like render @task as a shortcut for render :partial => @task. That would render the specified Task. The method render @project.tasks would render the _task.html.slim partial for each member of the collection.

Screenshot of trackapp with markers

A Project is rendered as a single link in the projects/_project.html.slim partial view, just containing of a link as a Project is only visualized as a switch at the sidebar (1).

app/views/projects/_project.html.slim

= link_to project.name, project_path(project), {class: "project project-#{project.id}", remote: true}

The Task partial in tasks/_task.html.slim shows a whole block (2), which will be replaced completely on any update referencing the task.

app/views/tasks/_task.html.slim

div[class="task task-#{task.id} #{'task-work' if task.work?} #{'task-done' if task.done?}"]

  .time #{format_logged(task.logged)}

  - if task.work?
    = link_to 'stop', stop_task_path(task), { class: 'task-action action-stop', remote: true }

  - if task.done?
    = link_to 'delete', task, { class: 'task-action action-delete', remote: true, method: :delete }

  - if !task.done? && !task.work?
    = link_to 'start', start_task_path(task), { class: 'task-action action-start', remote: true }

    = link_to 'done', finish_task_path(task), { class: 'task-action action-done', remote: true }

  .task-title
    - if task.work?
      span.icon &#9874;
    - elsif task.done?
      span.icon &#10003;
    - else
      span.icon &#9417;
    span #{task.name}
    - if !task.logs.empty?
      span.show-logs  &#8609;

  div[class="task-logs logs-#{task.id}"]
    = render task.logs

A single Log partial view is defined in logs/_log.html.slim and is rendered as a subset of a task partial (3).

app/views/logs/_log.html.slim

div[class="log#{' log-work' if !log.stop?}"]

  span.log-summary #{format_logged(log.duration)}

  span #{l(log.start, format: :long)} 

  - if log.stop
    span &#8611; #{l(log.stop, format: :long)}

The sidebar to switch through projects is described in the projects/index.html.slim file. The form tag creates a form with a single input field to submit a new project (4). The second one does the equivalent to an new task (5).

app/views/projects/index.html.slim

ul.tabs
  li
    = form_tag('/projects', method: :post, remote: true) do
      = text_field_tag(:name, nil, placeholder: 'New project')
  - @projects.each_with_index do |project, index|
    li[class="#{'active' if index == 0}#{' last' if index == @projects.size-1}"]
      = render project 

.project-time= format_logged(@project.logged)

= link_to 'delete project', projects_path(id: @project.id, format: 'js'), { class: 'project-delete', remote: true, method: :delete }

.task-create
  = form_tag('/tasks', method: :post, remote: true) do
    = text_field_tag(:name, nil, placeholder: 'New task')
    = hidden_field_tag(:project_id, "#{@projects.empty? ? 0 : @projects.first.id}")

.taskpane
  = render @project.tasks

To visually refresh any changes on a task object, the tasks/refresh.js.coffee JavaScript response replaces the task partial, updates the projects total time and highlight both elements to indicate a change. The method j() is an alias for escape_javascript, which makes any JavaScript rendered in the task partial non executable.

app/views/tasks/refresh.js.coffee

$('.logs-<%=@task.id%>').parents('.task:first').replaceWith '<%= j(render @task) %>'
notice $('.task-<%= @task.id %>'), '#fff68f'
$('.project-time').html '<%= escape_javascript(format_logged(@task.project.logged)) %>'
notice $('.project-time'), '#fff68f'
toggable $('.task-<%=@task.id%>')

The projects/create.js.coffee JavaScript response view switches between a successful project creation and a validation error. If the creation failed, the name attribute error will be shown in the only form field. If the creation was successful, the new project will be appended to the sidebar.

app/views/projects/create.js.coffee

<% if @project.valid? %>

$('.tabs li').removeClass 'last'
$('.tabs').append '<li class=\"last\"><%= j(render @project) %></li>'
notice $('.project-<%= @project.id %>'), '#eaeaae'
$('.tabs input').val ''

<% else %>

notice $('.tabs input'), '#ff6666'
$('.tabs input').val ''
$('.tabs input').attr 'placeholder', '<%= @project.errors[:name].first %>'

<% end %>

The tasks/create.js.coffee file describes the equivalent functionality for tasks.

app/views/tasks/create.js.coffee

<% if @task.valid? %>

$('.taskpane').prepend '<%= j(render @task) %>'
notice $('.task-<%= @task.id %>'), '#eaeaae'
$('.task-create #name').val ''

<% else %>

notice $('.task-create #name'), '#ff6666'
$('.task-create #name').val ''
$('.task-create #name').attr 'placeholder', '<%= @task.errors[:name].first %>'

<% end %>

Deleting, showing and switching to projects is handled in the projects/show.js.coffee file. Showing or switching to a project highlights the selected and replaces the taskpane with its tasks. If a project is deleted, the view partial for that deleted project is hidden and then the actual show statement is triggered.

app/views/projects/show.js.coffee

<% if !@deleted.nil? %>

notice $('.project-<%= @deleted.id %>'), '#ff6666'
$('.project-<%= @deleted.id %>').hide 'slow'
$('.tabs li:last-child').prev('li').addClass 'last'
notice $('.project-<%= @project.id %>'), '#fff68f'

<% end %>

$('.taskpane').replaceWith '<div class="taskpane"><%= j(render @project.tasks) %></div>'
$('.tabs li').removeClass 'active'
$('.project-<%= @project.id %>').parents('li:first').addClass 'active'
$('.task-create #project_id').val '<%= @project.id %>'
$('.project-delete').attr 'href', '<%= project_path(id: @project.id, format: 'js') %>'
$('.project-time').html '<%= j(format_logged(@project.logged)) %>'

toggable $('.task')

If a task is destroyed, tasks/destroy.js.coffee hides the corresponding view partial.

app/views/tasks/destroy.js.coffee

notice $('.task-<%= @task.id %>'), '#ff6666'
$('.task-<%= @task.id %>').hide 'slow'

Controller Tests

The ProjectsControllerTest checks each of the four methods: index, show, create and destroy. Test fixtures are used to help creating new objects like they were used in the model tests. The controller test basics provide methods to keep the test cases small.

Provided assertions are

There is also the block helper assert_difference to check if the count of the projects increased if a valid request hits the :create method.

test/controllers/projects_controller_test.rb

require 'test_helper'

class ProjectsControllerTest < ActionController::TestCase

  test 'should get project index' do
    get :index
    assert_response :success
    assigns(:project) == projects(:tree)
    assigns(:projects) == projects
    assert_template 'projects/index'
  end

  test 'should get project' do
    xhr :get, :show, id: 1, format: :js
    assert_response :success
    assigns(:project) == projects(:tree)
    assert_template 'projects/show'
  end

  test 'should create a new project' do
    assert_difference('Project.count') do
      xhr :post, :create, name: 'A test project', format: :js
    end
    assert_response :success
    assert_not_nil assigns(:project)
    assert_template 'projects/create'
  end

  test 'should delete a project' do
    deleted = projects(:tree)
    assert_difference('Project.count', -1) do
      xhr :delete, :destroy, id: 1, format: :js
    end
    assert_response :success
    assigns(:deleted) == deleted
    assigns(:project) == projects(:car)
    assert_template 'projects/show'
  end

end

In addition to the equivalent tests for tasks, the TasksControllerTest also includes assertions for changes on the depending logs.

test/controllers/tasks_controller_test.rb

require 'test_helper'

class TasksControllerTest < ActionController::TestCase

  test 'should create a new task' do
    assert_difference('Task.count') do
      xhr :post, :create, project_id: 1, name: 'A test task', format: :js
    end
    assert_response :success
    assert_not_nil assigns(:task)
    assert_template 'tasks/create'
  end

  test 'should close a task' do
    task = tasks(:unfinished)
    xhr :get, :finish, id: 2, format: :js
    assert task.reload.done?
    assert_response :success
    assigns(:task) == task
    assert_template 'tasks/refresh'
  end

  test 'should delete a task' do
    deleted = tasks(:finished)
    assert_difference('Task.count', -1) do
      xhr :delete, :destroy, id: 1, format: :js
    end
    assert_response :success
    assigns(:task) == deleted
    assert_template 'tasks/destroy'
  end

  test 'should create a new log on start' do
    assert_difference('tasks(:finished).logs.size') do
      xhr :get, :start, id: 1, format: :js
    end
    assert_response :success
    assigns(:task) == tasks(:finished)
    assigns(:log) == tasks(:finished).logs.last
    assert_template 'tasks/refresh'
  end

  test 'should update a log on stop' do
    xhr :get, :stop, id: 1, format: :js
    assert_response :success
    assert !tasks(:finished).logs.last.stop.nil?
    assigns(:task) == tasks(:finished)
    assigns(:log) == tasks(:finished).logs.last
    assert_template 'tasks/refresh'
  end

end

Now the test execution should create 9 small and happy dots, one for each non failing test case.

$ rake test:controllers

Seeding and Harvesting

The application would be empty on the first start and it would be be nice to preload it with some data. The way to do so is to define objects to start with in the seeds.rb file found in the db folder.

Defined is a project with finished, running, working and new tasks.

db/seeds.rb

...
project = Project.create(name: 'Cut a tree')

done = Task.create(name: 'Buy an axe', project: project, done: true)
Log.create(task: done, start: done.created_at, stop: done.created_at + 1.minute)

running = Task.create(name: 'Locate the tree', project: project)
Log.create(task: running, start: running.created_at, stop: running.created_at + 1.minute)
Log.create(task: running, start: running.created_at + 2.minutes, stop: running.created_at + 4.minutes)

work = Task.create(name: 'Chop chop chop', project: project)
Log.create(task: work, start: work.created_at, stop: work.created_at + 1.minute)
Log.create(task: work, start: work.created_at + 5.minutes, stop: work.created_at + 8.minutes)

Task.create(name: 'Take pictures', project: project)

The following command adds the data to the database.

$ rake db:seed

Testing with a Browser

The application should be complete. Boot it up to click around.

$ rails server

It starts at localhost:3000.

The Result

A working example of a single page application based on Ruby on Rails. It is not feature complete and the next steps to try on would be something like editing the names of projects and tasks or reopening tasks.

The full application can be found at the trackapp GitHub repository.