How I built a minimal Kanban Board with Ruby, Sinatra and Rails ActiveRecord

SVRourke
9 min readSep 29, 2020
Photo by Kelly Sikkema on Unsplash

Ideation

Interestingly enough, this project began out of necessity, while I was working on an entirely different project using a piece of cardboard and post-it notes as a make-shift Kanban board I became pretty frustrated with three aspects of my McGuyver’ed creation.

  1. The bottleneck on my throughput caused by the small size of post-it notes.
  2. The apathetic adhesive used by post-it notes.
  3. My inability to read my own handwriting.

So with my intended project securely placed on the back burner I decided to build myself a Kanban board using Ruby, Sinatra, and ActiveRecord.

Planning

At it’s core, a Kanban Board is a glorified to-do list with the to-dos separated by their current “done-ness”. A Kanban board might have:

  1. A Queue for tasks that have not been started yet.
  2. A section for tasks that are currently being worked on.
  3. A section for completed tasks under review
  4. A section for completed tasks which have passed review

My Kanban Board only really needs 3 sections: a queue section, an in-progress section, and a completed section.

Now that everyone has a basic understanding of how a Kanban Board works, lets discuss objects and entity relationship models.

My Kanban Board will use 3 entities:

  1. Users
  2. Projects
  3. Tasks

We’ll use the User model to keep track of who owns which projects, the Project model to keep track of which tasks go together, and finally the Task model to store the user inputted task, and the done-ness state of each task.

Entity Relationship Model

Structuring the entity relationships like so allows us to easily access everything through the User model

@user = User.find_by(:username => 'Sam')  # Create and array of a User's projects 
user_projects = @user.projects.all
# Create an array of a User's first project's tasks
tasks = user_projects.first.tasks

Now that we’ve worked out the entity relationships we can brainstorm some user stories to figure out what functionality we will need to build this app. I started this project for the purpose of making a personal tool for myself so I chose to limit the scope to a minimum viable product, the MVP user stories distilled down to essentially:

  • Being able to Log in
  • Create & Delete actions for User accounts (theoretically if there were multiple users present they would not be able to interact anyway so viewing accounts is unneeded and updating accounts doesn’t feel necessary in the scope of this project)
  • Full CRUD actions for projects
  • CUUD actions for Tasks (we do not need a separate read action for Tasks as the tasks will be displayed in the project view)

With our basic requirements laid out we can now figure out where each requirement will be met in our program.

Signing Up & Logging in

When you sign up for, or log into a website it’s usually at a page like “example.com/login”. Because we are accessing a top level page and function that requires no specifier like “users/3/posts” we can place our login and signup routes directly in the application controller.

user_controller

The only action that needs to be in the users controller is the user delete route.

project_controller

The Project model requires a route for each action in the CRUD acronym as well as a project index display route.

The Task

The Task model does not require a display action considering tasks will be displayed via the project display view.

We do require 2 distinct Update routes;

  1. one route for editing the content stored in a specific task.
  2. one route to increment a specific task’s done-ness state (from Queue to In Progress or In Progress to Completed).

Task model’s create route does not need a corresponding display route, we can use a simple form on the project display page to add tasks to a project.

Building

I set out to build this app using the ruby web application framework Sinatra and Rails ActiveRecord ORM. In addition I also used a ruby gem called Corneal which allows you to very quickly create a scaffold Sinatra app, though I found it very helpful, for this project specifically it may have been a bit extra and resulted in some bloat which I am convinced could probably have been avoided by spending more time reading the documentation but overall I would use it again.

Models

Each model has it’s own relationships and validations.

User Model

The User model will store the username, email, and password digest of the user, we will validate usernames and emails for uniqueness (to prevent duplicate accounts) and we will check that the password is at least 5 characters long.

We also establish the has many relationship between the User and Project models adding the ‘dependent destroy’ option so we don’t run into orphaned projects in the database when a user is deleted.

We then establish a relationship between User and Task models through their relationship with the project model

class User < ActiveRecord::Base
has_secure_password
validates :username, uniqueness: {
message: “Username Already Taken!”
}
validates :email, uniqueness: {
message: “Email Already In Use!”
}
validates :password, length: {
minimum: 5, message: “Password Must Be Longer Than 5 Chars!”
}
has_many :projects, dependent: :destroy
has_many :tasks, through: :projects
end

Project Model

The Project model will have title and description attributes and validations for the presence of both.

We also specify that a Project belongs to a User and that a Project has many tasks. We use the dependent delete option again on the relationship between Projects and Tasks to prevent orphaned tasks in the database when a project is deleted.

class Project < ActiveRecord::Base
validates :title, presence: {
message: "Project Must Have A Title!"
}
validates :description, presence: {
message: "Project Must Have A Description!"
}
belongs_to :user
has_many :tasks, dependent: :destroy
end

Task Model

The Task model has content and done-ness attributes and validates that a task does not already exist before saving to the database.

We also state that a Task belongs to a User as well as a Project

class Task < ActiveRecord::Base
validates :content, uniqueness: {
message: "Task already in project!"
}
validates :content, presence: {
message: "Cannot Save Blank Task!"
}
belongs_to :user
belongs_to :project
end

Migrations

Now that we’ve ironed out all of the model’s details we can move on to setting up the database.

create_users:

The User model stores 3 pieces of information

  1. Username
  2. password digest
  3. email (not really needed but we’ve come too far to turn back now)
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :username
t.string :password_digest
t.string :email
t.timestamps null: false
end
end
end

create_projects:

The Project model stores Title and Description strings as well as a reference to the User who created it

class CreateProjects < ActiveRecord::Migration
def change
create_table :projects do |t|
t.string :title
t.string :description
t.belongs_to :user
t.timestamps null: false
end
end
end

create_tasks:

The Task model stores a Content string and a Doneness state as well as a reference to the Project and User it belongs to.

class CreateTasks < ActiveRecord::Migration
def change
create_table :tasks do |t|
t.belongs_to :project
t.belongs_to :user
t.string :content
t.string :doneness
t.timestamps null: false
end
end
end

After running the migrations the database should be good to go, one tool I found immensely helpful in working out my Model relationships was the gem Tux which allows you to use Sinatra in a shell and interact with the database using your ActiveRecord models.

Views

Given that my goal was to create a minimum viable product for myself to use I didn’t allocate much effort to the front end, initially I wasted a lot of time trying out different CSS frameworks and eventually decided I could make a pretty minimal theme myself faster than I could figure out bootstrap. I won’t go super in-depth about what I did for the front end but I will cover a few concepts.

The views are pretty limited we have:

  1. Auth (Log In, Sign Up)
  2. Project (Index, Show, New)
  3. Edit Pages (Project, Task)

71% of those views will just be a page with a single form, which means we can style them all with the same stylesheet and we can serve some tasty copy-pasta minimal changes required. My flexible form styling is heavily reliant on flexbox which makes things like this very easy.

Basic Form

The Project Index view is a simple flexbox based column populated with cards for each Project the User owns. The project cards contain the project title and Task counts for each done-ness state.

Projects Index

The Project Show View displays a form to quickly add tasks to the board. The board is a three column grid based layout (one column for each of the task doneness states) each containing a flexbox based column populated with task cards.

The task cards contain the task content as well as a link to the task edit page, a button to increment the task’s doneness state and a button to delete the task.

Project a Page

The final view not mentioned previously is the header template that is rendered by the layout along all of the other views except the login and sign up views. The header consists of a nav containing a link to a user’s projects page, a logout link, a new project link and a delete account link. If the user is looking at a project the nav also contains a delete project link and an edit project link. The header also displays the user’s name on all pages except the project view in which case it displays the project’s title and description.

The layout displays the header template at the top of the screen as long as you are not on the signup or login pages, the views are rendered in a grid using place-items: center; to make sure the views are centered vertically and horizontally within the view display area.

Errors

Our models use ActiveRecord validations to ensure we don’t save bad data to the database. If errors are present they are accessible by calling errors on the object like so:

@project = Project.new(:title => "Project")
@project.errors
~$ {:description => "Project Requires A Description!"}

we can easily set session[:errors] to a hash of the errors from any object we were trying to create or edit and then conditionally display them in the view using erb, just remember to clear the session[:errors] hash after the last error message is displayed on the page.

Lessons Learned

The biggest lesson I learned while building this tool was the importance of clearly defining a scope and sticking to it. I wasted so much time fumbling in the dark getting distracted by unnecessary functionality when what I actually wanted was so much easier.

In a similar way, I spent decent amount of time trying several css frameworks hoping I could Plug-and-play, maybe if I had spent a bit more time with any of them I would’ve gotten to that point so I guess the moral of the story is pick something and commit or drop it immediately but ambiguity eats time like it’s going out of style.

I’ve know for a while that it is a waste of time to reinvent the wheel, what I learned through this project was that I can’t always tell if I’m working on a wheel or something else, originally I implemented the model validations by hand in the controller routes, checking input and passing messages to the session in a somewhat clumsy way. I only realized ActiveRecord had a validation system I could use after I had a fully functional app, at which point I had to go back through all of my code and convert it to using the AR validations.

If you enjoyed my writing feel free to check out my website and connect with me on LinkedIn

--

--