Add todo categories and due dates with filtering capabilities

- Added category field to Todo model for organizing todos
- Added due_date field with timezone support for deadline tracking
- Enhanced CRUD operations with filtering by category, completion status, and overdue items
- Added new API endpoints for category-based and overdue todo retrieval
- Updated API documentation with new filtering query parameters
- Created database migration for new fields with proper indexing
- Updated README with comprehensive feature documentation
This commit is contained in:
Automated Action 2025-06-17 05:51:35 +00:00
parent 7b3cd8d0dd
commit 091c42482c
6 changed files with 129 additions and 8 deletions

View File

@ -5,6 +5,9 @@ A simple Todo application API built with FastAPI and SQLite.
## Features
- ✅ Create, read, update, and delete todos
- ✅ Todo categories/tags for organization
- ✅ Due dates with overdue detection
- ✅ Advanced filtering (by category, completion status, overdue)
- ✅ SQLite database with SQLAlchemy ORM
- ✅ Database migrations with Alembic
- ✅ API documentation with Swagger UI
@ -54,11 +57,14 @@ The API is available at `http://localhost:8000`
- **GET** `/health` - Health check endpoint
### Todo Endpoints
- **GET** `/api/v1/todos/` - List all todos (with pagination)
- **GET** `/api/v1/todos/` - List all todos (with pagination and filtering)
- Query parameters: `skip`, `limit`, `category`, `completed`, `overdue_only`
- **POST** `/api/v1/todos/` - Create a new todo
- **GET** `/api/v1/todos/{todo_id}` - Get a specific todo
- **PUT** `/api/v1/todos/{todo_id}` - Update a specific todo
- **DELETE** `/api/v1/todos/{todo_id}` - Delete a specific todo
- **GET** `/api/v1/todos/categories/{category}` - Get all todos by category
- **GET** `/api/v1/todos/overdue` - Get all overdue todos
## Database
@ -71,6 +77,8 @@ The application uses SQLite database stored at `/app/storage/db/db.sqlite`.
- `title` (String, Required)
- `description` (String, Optional)
- `completed` (Boolean, Default: False)
- `category` (String, Optional) - For organizing todos
- `due_date` (DateTime with timezone, Optional) - When the todo is due
- `created_at` (DateTime with timezone)
- `updated_at` (DateTime with timezone)

View File

@ -1,6 +1,6 @@
from typing import List
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from app.crud import todo
@ -14,15 +14,47 @@ router = APIRouter()
def read_todos(
skip: int = 0,
limit: int = 100,
category: Optional[str] = Query(None, description="Filter by category"),
completed: Optional[bool] = Query(None, description="Filter by completion status"),
overdue_only: bool = Query(False, description="Show only overdue todos"),
db: Session = Depends(get_db),
):
"""
Retrieve todos with pagination.
Retrieve todos with pagination and filtering.
"""
todos = todo.get_multi(db, skip=skip, limit=limit)
todos = todo.get_multi(
db,
skip=skip,
limit=limit,
category=category,
completed=completed,
overdue_only=overdue_only,
)
return todos
@router.get("/categories/{category}", response_model=List[TodoResponse])
def read_todos_by_category(
*,
db: Session = Depends(get_db),
category: str,
):
"""
Get all todos by category.
"""
return todo.get_by_category(db=db, category=category)
@router.get("/overdue", response_model=List[TodoResponse])
def read_overdue_todos(
db: Session = Depends(get_db),
):
"""
Get all overdue todos.
"""
return todo.get_overdue(db=db)
@router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
def create_todo(
*,

View File

@ -1,6 +1,8 @@
from typing import List, Optional
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy import and_, not_
from app.models.todo import Todo
from app.schemas.todo import TodoCreate, TodoUpdate
@ -13,9 +15,33 @@ class CRUDTodo:
"""Get a single todo by ID."""
return db.query(Todo).filter(Todo.id == todo_id).first()
def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Todo]:
"""Get multiple todos with pagination."""
return db.query(Todo).offset(skip).limit(limit).all()
def get_multi(
self,
db: Session,
*,
skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
completed: Optional[bool] = None,
overdue_only: bool = False,
) -> List[Todo]:
"""Get multiple todos with pagination and filtering."""
query = db.query(Todo)
filters = []
if category:
filters.append(Todo.category == category)
if completed is not None:
filters.append(Todo.completed == completed)
if overdue_only:
filters.append(
and_(Todo.due_date < datetime.now(), not_(Todo.completed))
)
if filters:
query = query.filter(and_(*filters))
return query.offset(skip).limit(limit).all()
def create(self, db: Session, *, obj_in: TodoCreate) -> Todo:
"""Create a new todo."""
@ -23,6 +49,8 @@ class CRUDTodo:
title=obj_in.title,
description=obj_in.description,
completed=obj_in.completed,
category=obj_in.category,
due_date=obj_in.due_date,
)
db.add(db_obj)
db.commit()
@ -47,5 +75,17 @@ class CRUDTodo:
db.commit()
return obj
def get_by_category(self, db: Session, *, category: str) -> List[Todo]:
"""Get all todos by category."""
return db.query(Todo).filter(Todo.category == category).all()
def get_overdue(self, db: Session) -> List[Todo]:
"""Get all overdue todos."""
return (
db.query(Todo)
.filter(and_(Todo.due_date < datetime.now(), not_(Todo.completed)))
.all()
)
todo = CRUDTodo()

View File

@ -13,5 +13,7 @@ class Todo(Base):
title = Column(String, index=True)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
category = Column(String, nullable=True, index=True)
due_date = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

View File

@ -10,6 +10,8 @@ class TodoBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
category: Optional[str] = None
due_date: Optional[datetime] = None
class TodoCreate(TodoBase):
@ -24,6 +26,8 @@ class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
category: Optional[str] = None
due_date: Optional[datetime] = None
class TodoResponse(TodoBase):

View File

@ -0,0 +1,35 @@
"""Add category and due_date to todos table
Revision ID: 002
Revises: 001
Create Date: 2025-06-17 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "002"
down_revision = "001"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("todos", sa.Column("category", sa.String(), nullable=True))
op.add_column(
"todos", sa.Column("due_date", sa.DateTime(timezone=True), nullable=True)
)
op.create_index(op.f("ix_todos_category"), "todos", ["category"], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_todos_category"), table_name="todos")
op.drop_column("todos", "due_date")
op.drop_column("todos", "category")
# ### end Alembic commands ###