Skip to content

Instantly share code, notes, and snippets.

@SamSamskies
Last active December 24, 2015 17:09
Show Gist options
  • Save SamSamskies/6832950 to your computer and use it in GitHub Desktop.
Save SamSamskies/6832950 to your computer and use it in GitHub Desktop.
ActiveRecord TODO MVC example

This is an example MVC solution of the ActiveRecord TODOs: Part 1 challenge. The purpose of this document is to walk you through my thought process in creating this solution.

As always, the first thing I did was break this down into smaller problems. I decided to setup the database and then start with the view because I wanted to make an MVP as quickly as possible. Here's what I started out with:

Initial View

require_relative 'config/application'

def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

if ARGV.any?

  case ARGV[0]
    when "list"
      puts "list"
    when "add"
      puts "add"
    when "delete"
      puts "delete"
    when "complete"
      puts "complete"
    else
      puts "invalid command"
      display_menu
  end

else
  display_menu
end

With this code in place, I was able to get my program up and running in minutes. It didn't do anything useful yet, but it worked. It could accept all of the commands and repeat them back to you or tell you if you've entered an invalid command. Sweet. What next?

I decided to tackle one feature at a time, so up first was listing my tasks. When using MVC, you should let the controller do the communicating with the Model. So instead of plopping Task.all in the view and then looping through and printing each task, I asked the controller to talk to my model to go fetch all the tasks for me.

View With List Functionality

require_relative 'config/application'
require_relative 'app/controllers/tasks_controller'

def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

def handle_list_command
  tasks = TasksController.list

  if tasks.empty?
    puts
    puts "Woohoo no tasks to complete yet!"
    puts
  else
    tasks.each_with_index { |task, i| puts "#{i}.\t#{task.name}"}
  end
end

if ARGV.any?

  case ARGV[0]
    when "list"
      handle_list_command
    when "add"
      puts "add"
    when "delete"
      puts "delete"
    when "complete"
      puts "complete"
    else
      puts "invalid command"
      display_menu
  end

else
  display_menu
end

Initial Contoller

class TasksController

  def self.list
    Task.all
  end
end

Notice how I extracted the code to handle the list command in the view. This way it's easier to read and update. Also, it's important to note how I do all the printing in view and not in the controller. Remember to sepearte your concerns.

BOOM! That seemed to be working, but I couldn't know for sure unless I seeded the database. Faker to the rescue! I made a simple seed file that I could always run if I ever wanted to add more random tasks to my database.

Seed File

require 'faker'

5.times do
  Task.create(name: Faker::Lorem.sentence)
end

Now with the seed file in place I could test my list command and move on to the next feature. Up next, add.

View With Add Functionality

require_relative 'config/application'
require_relative 'app/controllers/tasks_controller'

def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

def handle_list_command
  tasks = TasksController.list

  if tasks.empty?
    puts
    puts "Woohoo no tasks to complete yet!"
    puts
  else
    tasks.each_with_index { |task, i| puts "#{i+1}.".ljust(4) + task.name }
  end
end

def handle_add_command(sentence)
  TasksController.add sentence
  puts "Appended #{sentence} to your TODO list..."
end

if ARGV.any?

  case ARGV[0]
    when "list"
      handle_list_command
    when "add"
      handle_add_command ARGV[1..-1].join(' ')
    when "delete"
      puts "delete"
    when "complete"
      puts "complete"
    else
      puts "invalid command"
      display_menu
  end

else
  display_menu
end

Controller With Add Functionality

class TasksController < ActiveRecord::Base

  def self.list
    Task.all
  end

  def self.add(sentence)
    Task.create(name: sentence)
  end
end

Everything works fine like this, but what about if the user doesn't enter a task or if they enter a duplicate task? Do you want to add blank or duplicate tasks onto your list? This is where Active Record validations come to the rescue and we can finally put some code into our model.

Model With Validations

class Task < ActiveRecord::Base
  validates_presence_of :name, message: "Task can't be blank."
  validates_uniqueness_of :name, message: 'Task is already on the list.'
end

Controller With Error Handling For Add

class TasksController

  def self.list
    Task.all
  end

  def self.add(sentence)
    task = Task.create(name: sentence)
    task.valid? ? "Appended #{sentence} to your TODO list..." : "Error: #{task.errors.messages[:name].first}"
  end
end

Notice again that I didn't print anything in the controller. I'm returning the correct output to the view and letting the view handle the printing.

View With Error Handling For Add

require_relative 'config/application'
require_relative 'app/controllers/tasks_controller'

def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

def handle_list_command
  tasks = TasksController.list

  if tasks.empty?
    puts
    puts "Woohoo no tasks to complete yet!"
    puts
  else
    tasks.each_with_index { |task, i| puts "#{i+1}.".ljust(4) + task.name }
  end
end

def handle_add_command(sentence)
  puts TasksController.add sentence
end

if ARGV.any?

  case ARGV[0]
    when "list"
      handle_list_command
    when "add"
      handle_add_command ARGV[1..-1].join(' ')
    when "delete"
      puts "delete"
    when "complete"
      puts "complete"
    else
      puts "invalid command"
      display_menu
  end

else
  display_menu
end

Notice how I handled all the error handling logic in the controller. All I did in the view was print out the response from the controller.

I tackled the the rest of the features in the same step by step manner until I was done. Here's what the final view and controller ended up looking like. I never added anything else to the model.

Final View

require_relative 'config/application'
require_relative 'app/controllers/tasks_controller'

def execute_todo_app
  if ARGV.any?

    case ARGV[0]
      when "list"
        handle_list_command
      when "add"
        handle_add_command ARGV[1..-1].join(' ')
      when "delete"
        handle_delete_command ARGV[1]
      when "complete"
        handle_complete_command ARGV[1]
      when "help"
        display_menu
      else
        puts "invalid command"
        display_menu
    end

  else
    display_menu
  end
end

def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

def handle_list_command
  tasks = TasksController.list

  if tasks.empty?
    puts
    puts "Woohoo no tasks to complete yet!"
    puts
  else
    tasks.each_with_index do |task, i|
      completed = task.completed ? 'x' : ' '
      puts "#{i+1}.".ljust(4) + "[#{completed}] #{task.name}"
    end
  end
end

def handle_add_command(sentence)
  puts TasksController.add sentence
end

def handle_delete_command(task_id)
  puts TasksController.delete task_id.to_i
end

def handle_complete_command(task_id)
  puts TasksController.complete task_id.to_i
end


### Program execution starts here ###

execute_todo_app

Final Controller

class TasksController

  def self.list
    Task.all
  end

  def self.add(sentence)
    task = Task.create(name: sentence)
    task.valid? ? "Appended #{sentence} to your TODO list..." : "Error: #{task.errors.messages[:name].first}"
  end

  # Note this is not the id in the database. This id identifies where on the list the task is.
  def self.delete(task_id)
    task = find_task task_id

    if task
      task = task.destroy
      task.valid? ? "Deleted '#{task.name}' from your TODO list..." : "Error: Something went wrong. Please try again later."
    else
      "Error: invalid task ID provided."
    end
  end

  def self.complete(task_id)
    task = find_task task_id

    if task
      update_result = task.update_attributes completed: true
      update_result ? "Completed '#{task.name}' from your TODO list..." : "Error: Something went wrong. Please try again later."
    else
      "Error: invalid task ID provided."
    end

  end

  def self.find_task(task_id)
    tasks = Task.all

    (task_id > tasks.count or task_id < 1) ? nil : tasks[task_id - 1]
  end
end

After thinking about this more, I decided that there was unnecessary entanglement of the view in the main part of the application, so I separated the view from the main app.

Refactored Main Part of Application

require_relative 'config/application'

def execute_todo_app
  if ARGV.any?

    case ARGV[0]
      when "list"
        TasksController.list
      when "add"
        TasksController.add ARGV[1..-1].join(' ')
      when "delete"
        TasksController.delete ARGV[1].to_i
      when "complete"
        TasksController.complete ARGV[1].to_i
      when "help"
        TasksController.menu
      else
        TasksController.invalid_command
    end

  else
    TasksController.menu
  end
end


### Program execution starts here ###

execute_todo_app

Refactored Controller

class TasksController

  def self.menu
    TasksView.display_menu
  end

  def self.invalid_command
    TasksView.display_invalid_command
    TasksView.display_menu
  end

  def self.list
    TasksView.display_list Task.all
  end

  def self.add(sentence)
    task = Task.create(name: sentence)
    if task.valid?
      TasksView.display_notice "Appended #{sentence} to your TODO list..."
    else
      TasksView.display_notice "Error: #{task.errors.messages[:name].first}"
    end
  end

  # Note this is not the id in the database. This id identifies where on the list the task is.
  def self.delete(task_id)
    task = find_task task_id

    if task
      task = task.destroy
      if task.valid?
        TasksView.display_notice "Deleted '#{task.name}' from your TODO list..."
      else
        TasksView.display_notice "Error: Something went wrong. Please try again later."
      end
    else
      TasksView.display_notice "Error: invalid task ID provided."
    end
  end

  def self.complete(task_id)
    task = find_task task_id

    if task
      update_result = task.update_attributes completed: true
      if update_result
        TasksView.display_notice "Completed '#{task.name}' from your TODO list..."
      else
        TasksView.display_notice "Error: Something went wrong. Please try again later."
      end
    else
      TasksView.display_notice "Error: invalid task ID provided."
    end

  end

  def self.find_task(task_id)
    tasks = Task.all

    (task_id > tasks.count or task_id < 1) ? nil : tasks[task_id - 1]
  end
end

Refactored View

class TasksView
  def self.display_menu
    puts
    puts "*" * 100
    puts "Usage:"
    puts "ruby todo.rb list \t\t\t\t # List all tasks"
    puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
    puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
    puts "ruby todo.rb complete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
    puts
  end

  def self.display_invalid_command
    puts "Invalid command :/"
  end

  def self.display_list(tasks)
    if tasks.empty?
      puts
      puts "Woohoo no tasks to complete yet!"
      puts
    else
      tasks.each_with_index do |task, i|
        completed = task.completed ? 'x' : ' '
        puts "#{i+1}.".ljust(4) + "[#{completed}] #{task.name}"
      end
    end
  end

  def self.display_notice(notice)
    puts notice
  end
end

The final solution can be found at https://github.com/SamSamskies/ar-todo. Please feel free to email me at samprofessional@gmail.com if you have any questions, would like a code review, want to pair with me on refactoring your code, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment