Follow Ruby Style Guide and Rails conventions.
class Task < ApplicationRecord
# 1. Associations
belongs_to :project
# 2. Validations
validates :title, presence: true
validates :status, presence: true, inclusion: { in: %w[todo in_progress done] }
validates :priority, presence: true, numericality: { only_integer: true, in: 1..5 }
# 3. Scopes
scope :with_status, ->(status) { where(status: status) if status.present? }
scope :overdue, -> { where('due_date < ? AND status != ?', Date.today, 'done') }
scope :sorted_by, ->(sort_param) {
case sort_param
when 'priority_desc' then order(priority: :asc) # 1 is highest
when 'due_date_asc' then order(Arel.sql('due_date IS NULL, due_date ASC'))
else all
end
}
# 4. Instance methods
def overdue?
due_date.present? && due_date < Date.today && status != 'done'
end
endKeep controllers thin - use scopes for filtering.
class ProjectsController < ApplicationController
def index
@projects = Project.includes(:tasks)
end
def show
@project = Project.find(params[:id])
@tasks = @project.tasks
.with_status(params[:status])
.sorted_by(params[:sort])
end
def create
@project = Project.new(project_params)
if @project.save
redirect_to @project, notice: 'Project created successfully.'
else
render :new
end
end
private
def project_params
params.require(:project).permit(:name, :description)
end
endReturn JSON with computed fields.
module Api
class TasksController < ApplicationController
def index
project = Project.find(params[:project_id])
tasks = project.tasks.with_status(params[:status])
tasks = tasks.overdue if params[:overdue] == 'true'
render json: tasks.map { |task|
{
id: task.id,
title: task.title,
status: task.status,
priority: task.priority,
due_date: task.due_date,
overdue: task.overdue?
}
}
end
end
endUse forms with proper error handling.
<%= form_with(model: [@project, @task]) do |form| %>
<% if @task.errors.any? %>
<div class="errors">
<h3><%= pluralize(@task.errors.count, "error") %> prohibited this task from being saved:</h3>
<ul>
<% @task.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :status %>
<%= form.select :status, [['To Do', 'todo'], ['In Progress', 'in_progress'], ['Done', 'done']] %>
</div>
<div class="field">
<%= form.label :priority %>
<%= form.select :priority, (1..5).to_a %>
</div>
<div class="field">
<%= form.label :due_date %>
<%= form.date_field :due_date %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>Prevent N+1 queries - use includes:
# Bad - N+1 query
@projects = Project.all
@projects.each { |project| puts project.tasks.count }
# Good
@projects = Project.includes(:tasks)
@projects.each { |project| puts project.tasks.size }RESTful routes with API namespace.
Rails.application.routes.draw do
resources :projects do
resources :tasks
end
namespace :api do
resources :projects, only: [] do
resources :tasks, only: [:index]
end
end
endRSpec.describe Task, type: :model do
describe 'validations' do
it { should belong_to(:project) }
it { should validate_presence_of(:title) }
it { should validate_inclusion_of(:status).in_array(%w[todo in_progress done]) }
it { should validate_numericality_of(:priority).is_in(1..5) }
end
describe '#overdue?' do
it 'returns false when due date is in future' do
task = build(:task, due_date: 1.day.from_now, status: 'todo')
expect(task.overdue?).to be false
end
it 'returns true when due date is past and not done' do
task = build(:task, due_date: 1.day.ago, status: 'todo')
expect(task.overdue?).to be true
end
it 'returns false when past due but done' do
task = build(:task, due_date: 1.day.ago, status: 'done')
expect(task.overdue?).to be false
end
end
describe 'scopes' do
let(:project) { create(:project) }
describe '.with_status' do
it 'filters by status' do
todo = create(:task, project: project, status: 'todo')
done = create(:task, project: project, status: 'done')
expect(Task.with_status('todo')).to include(todo)
expect(Task.with_status('todo')).not_to include(done)
end
end
describe '.overdue' do
it 'returns only overdue tasks' do
overdue = create(:task, project: project, due_date: 1.day.ago, status: 'todo')
not_overdue = create(:task, project: project, due_date: 1.day.from_now)
expect(Task.overdue).to include(overdue)
expect(Task.overdue).not_to include(not_overdue)
end
end
end
endRSpec.describe 'API Tasks', type: :request do
let(:project) { create(:project) }
describe 'GET /api/projects/:project_id/tasks' do
it 'returns tasks as JSON' do
task = create(:task, project: project, title: 'Test Task')
get "/api/projects/#{project.id}/tasks"
expect(response).to have_http_status(:success)
json = JSON.parse(response.body)
expect(json.first['title']).to eq('Test Task')
expect(json.first).to have_key('overdue')
end
it 'filters by status' do
create(:task, project: project, status: 'todo')
create(:task, project: project, status: 'done')
get "/api/projects/#{project.id}/tasks?status=todo"
json = JSON.parse(response.body)
expect(json.length).to eq(1)
expect(json.first['status']).to eq('todo')
end
it 'filters overdue tasks' do
create(:task, project: project, due_date: 1.day.ago, status: 'todo')
create(:task, project: project, due_date: 1.day.from_now, status: 'todo')
get "/api/projects/#{project.id}/tasks?overdue=true"
json = JSON.parse(response.body)
expect(json.length).to eq(1)
expect(json.first['overdue']).to be true
end
end
end- Use strong parameters in controllers
- Validate all user input
- Parameterized queries (Rails default)
- CSRF protection enabled (Rails default)