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:
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.
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
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.
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.
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
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.
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
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.
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.
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
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.
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
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
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 [email protected] if you have any questions, would like a code review, want to pair with me on refactoring your code, etc.