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
|
## Features
|
||||||
|
|
||||||
- ✅ Create, read, update, and delete todos
|
- ✅ 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
|
- ✅ SQLite database with SQLAlchemy ORM
|
||||||
- ✅ Database migrations with Alembic
|
- ✅ Database migrations with Alembic
|
||||||
- ✅ API documentation with Swagger UI
|
- ✅ API documentation with Swagger UI
|
||||||
@ -54,11 +57,14 @@ The API is available at `http://localhost:8000`
|
|||||||
- **GET** `/health` - Health check endpoint
|
- **GET** `/health` - Health check endpoint
|
||||||
|
|
||||||
### Todo Endpoints
|
### 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
|
- **POST** `/api/v1/todos/` - Create a new todo
|
||||||
- **GET** `/api/v1/todos/{todo_id}` - Get a specific todo
|
- **GET** `/api/v1/todos/{todo_id}` - Get a specific todo
|
||||||
- **PUT** `/api/v1/todos/{todo_id}` - Update a specific todo
|
- **PUT** `/api/v1/todos/{todo_id}` - Update a specific todo
|
||||||
- **DELETE** `/api/v1/todos/{todo_id}` - Delete 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
|
## Database
|
||||||
|
|
||||||
@ -71,6 +77,8 @@ The application uses SQLite database stored at `/app/storage/db/db.sqlite`.
|
|||||||
- `title` (String, Required)
|
- `title` (String, Required)
|
||||||
- `description` (String, Optional)
|
- `description` (String, Optional)
|
||||||
- `completed` (Boolean, Default: False)
|
- `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)
|
- `created_at` (DateTime with timezone)
|
||||||
- `updated_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 sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.crud import todo
|
from app.crud import todo
|
||||||
@ -14,15 +14,47 @@ router = APIRouter()
|
|||||||
def read_todos(
|
def read_todos(
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 100,
|
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),
|
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
|
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)
|
@router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
|
||||||
def create_todo(
|
def create_todo(
|
||||||
*,
|
*,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, not_
|
||||||
|
|
||||||
from app.models.todo import Todo
|
from app.models.todo import Todo
|
||||||
from app.schemas.todo import TodoCreate, TodoUpdate
|
from app.schemas.todo import TodoCreate, TodoUpdate
|
||||||
@ -13,9 +15,33 @@ class CRUDTodo:
|
|||||||
"""Get a single todo by ID."""
|
"""Get a single todo by ID."""
|
||||||
return db.query(Todo).filter(Todo.id == todo_id).first()
|
return db.query(Todo).filter(Todo.id == todo_id).first()
|
||||||
|
|
||||||
def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Todo]:
|
def get_multi(
|
||||||
"""Get multiple todos with pagination."""
|
self,
|
||||||
return db.query(Todo).offset(skip).limit(limit).all()
|
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:
|
def create(self, db: Session, *, obj_in: TodoCreate) -> Todo:
|
||||||
"""Create a new todo."""
|
"""Create a new todo."""
|
||||||
@ -23,6 +49,8 @@ class CRUDTodo:
|
|||||||
title=obj_in.title,
|
title=obj_in.title,
|
||||||
description=obj_in.description,
|
description=obj_in.description,
|
||||||
completed=obj_in.completed,
|
completed=obj_in.completed,
|
||||||
|
category=obj_in.category,
|
||||||
|
due_date=obj_in.due_date,
|
||||||
)
|
)
|
||||||
db.add(db_obj)
|
db.add(db_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
@ -47,5 +75,17 @@ class CRUDTodo:
|
|||||||
db.commit()
|
db.commit()
|
||||||
return obj
|
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()
|
todo = CRUDTodo()
|
||||||
|
@ -13,5 +13,7 @@ class Todo(Base):
|
|||||||
title = Column(String, index=True)
|
title = Column(String, index=True)
|
||||||
description = Column(String, nullable=True)
|
description = Column(String, nullable=True)
|
||||||
completed = Column(Boolean, default=False)
|
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())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
@ -10,6 +10,8 @@ class TodoBase(BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
completed: bool = False
|
completed: bool = False
|
||||||
|
category: Optional[str] = None
|
||||||
|
due_date: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class TodoCreate(TodoBase):
|
class TodoCreate(TodoBase):
|
||||||
@ -24,6 +26,8 @@ class TodoUpdate(BaseModel):
|
|||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
completed: Optional[bool] = None
|
completed: Optional[bool] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
due_date: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class TodoResponse(TodoBase):
|
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