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:
parent
7b3cd8d0dd
commit
091c42482c
10
README.md
10
README.md
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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):
|
||||
|
35
migrations/versions/002_add_category_and_due_date.py
Normal file
35
migrations/versions/002_add_category_and_due_date.py
Normal 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 ###
|
Loading…
x
Reference in New Issue
Block a user