Add categories, priorities and due dates to Todo app

- Added category, priority and due_date fields to Todo model
- Created Enum for priority levels (low, medium, high)
- Added advanced filtering in CRUD and API routes
- Added statistics endpoint for todo analytics
- Created Alembic migration for new fields
- Updated README with new feature documentation

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-13 03:56:40 +00:00
parent 7e35149610
commit 357c328042
6 changed files with 272 additions and 23 deletions

View File

@ -1,11 +1,21 @@
# Simple Todo Application - FastAPI # Simple Todo Application - FastAPI
A simple Todo API application built with FastAPI and SQLite. This RESTful API provides endpoints to create, read, update, and delete todo items. A simple Todo API application built with FastAPI and SQLite. This RESTful API provides endpoints to create, read, update, and delete todo items with advanced organization features.
## Features ## Features
- RESTful API for Todo management - RESTful API for Todo management
- CRUD operations for Todo items - CRUD operations for Todo items
- Todo categorization with categories
- Priority levels (low, medium, high)
- Due dates for task deadlines
- Advanced filtering capabilities:
- Filter by category
- Filter by priority
- Filter by completion status
- Filter by due date range
- Customizable sorting options
- Todo statistics and analytics
- SQLAlchemy ORM with SQLite database - SQLAlchemy ORM with SQLite database
- Alembic for database migrations - Alembic for database migrations
- FastAPI's automatic OpenAPI documentation - FastAPI's automatic OpenAPI documentation
@ -38,15 +48,35 @@ simpletodoapplication/
## API Endpoints ## API Endpoints
- **GET /api/v1/todos/** - Get all todos ### Todo Operations
- **GET /api/v1/todos/** - Get all todos with filtering options
- **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 todo - **PUT /api/v1/todos/{todo_id}** - Update a todo
- **DELETE /api/v1/todos/{todo_id}** - Delete a todo - **DELETE /api/v1/todos/{todo_id}** - Delete a todo
### Statistics
- **GET /api/v1/todos/stats** - Get todo statistics and analytics
### System
- **GET /health** - Health check endpoint - **GET /health** - Health check endpoint
- **GET /docs** - API documentation (Swagger UI) - **GET /docs** - API documentation (Swagger UI)
- **GET /redoc** - Alternative API documentation (ReDoc) - **GET /redoc** - Alternative API documentation (ReDoc)
## Filtering and Sorting
The GET /api/v1/todos/ endpoint supports the following query parameters:
- **category** - Filter todos by category
- **priority** - Filter by priority level (low, medium, high)
- **completed** - Filter by completion status (true/false)
- **due_before** - Filter todos due before a specific date
- **due_after** - Filter todos due after a specific date
- **sort_by** - Sort by any todo field (default: created_at)
- **sort_desc** - Sort in descending order (default: true)
- **skip** - Number of records to skip (for pagination)
- **limit** - Maximum number of records to return (for pagination)
## Setup and Running ## Setup and Running
1. Install dependencies: 1. Install dependencies:
@ -77,7 +107,34 @@ simpletodoapplication/
"title": "Sample Todo", "title": "Sample Todo",
"description": "This is a sample todo item", "description": "This is a sample todo item",
"completed": false, "completed": false,
"category": "work",
"priority": "high",
"due_date": "2025-05-20T12:00:00",
"created_at": "2025-05-13T12:00:00", "created_at": "2025-05-13T12:00:00",
"updated_at": null "updated_at": null
} }
``` ```
## Statistics
The /api/v1/todos/stats endpoint returns the following information:
```json
{
"total": 10,
"completed": 3,
"incomplete": 7,
"overdue": 2,
"by_category": {
"work": 4,
"personal": 3,
"shopping": 2,
"uncategorized": 1
},
"by_priority": {
"high": 3,
"medium": 5,
"low": 2
}
}
```

View File

@ -0,0 +1,45 @@
"""add category priority and due date
Revision ID: 5f42ebcf5b2e
Revises: 4f42ebcf5b2d
Create Date: 2025-05-13
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5f42ebcf5b2e'
down_revision = '4f42ebcf5b2d'
branch_labels = None
depends_on = None
def upgrade():
# Create an enum type for priority
op.execute("CREATE TYPE priority_enum AS ENUM ('low', 'medium', 'high')")
# Add new columns
op.add_column('todos', sa.Column('category', sa.String(), nullable=True))
op.add_column('todos', sa.Column('priority', sa.Enum('low', 'medium', 'high', name='priority_enum'), nullable=True))
op.add_column('todos', sa.Column('due_date', sa.DateTime(timezone=True), nullable=True))
# Create index on category for faster queries
op.create_index(op.f('ix_todos_category'), 'todos', ['category'], unique=False)
# Set default values
op.execute("UPDATE todos SET priority = 'medium' WHERE priority IS NULL")
def downgrade():
# Drop index
op.drop_index(op.f('ix_todos_category'), table_name='todos')
# Drop columns
op.drop_column('todos', 'due_date')
op.drop_column('todos', 'priority')
op.drop_column('todos', 'category')
# Drop enum type
op.execute("DROP TYPE priority_enum")

View File

@ -1,27 +1,68 @@
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 typing import List from typing import List, Optional, Dict, Any
from datetime import datetime
from app import crud from app import crud
from app.schemas.todo import Todo, TodoCreate, TodoUpdate from app.models.todo import PriorityEnum as ModelPriorityEnum
from app.schemas.todo import Todo, TodoCreate, TodoUpdate, PriorityEnum
from app.db.base import get_db from app.db.base import get_db
router = APIRouter() router = APIRouter()
@router.get("/", response_model=List[Todo]) @router.get("/", response_model=List[Todo])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): def read_todos(
todos = crud.get_todos(db, skip=skip, limit=limit) skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
priority: Optional[PriorityEnum] = None,
completed: Optional[bool] = None,
due_before: Optional[datetime] = None,
due_after: Optional[datetime] = None,
sort_by: str = "created_at",
sort_desc: bool = True,
db: Session = Depends(get_db)
):
"""
Get all todos with filtering options:
- Filter by category
- Filter by priority level
- Filter by completion status
- Filter by due date (before/after)
- Sort by any field
- Pagination
"""
todos = crud.get_todos(
db=db,
skip=skip,
limit=limit,
category=category,
priority=priority.value if priority else None,
completed=completed,
due_before=due_before,
due_after=due_after,
sort_by=sort_by,
sort_desc=sort_desc
)
return todos return todos
@router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
"""Create a new todo item with extended fields"""
return crud.create_todo(db=db, todo=todo) return crud.create_todo(db=db, todo=todo)
@router.get("/stats", response_model=Dict[str, Any])
def get_todo_stats(db: Session = Depends(get_db)):
"""Get statistics about todos"""
return crud.get_todo_stats(db=db)
@router.get("/{todo_id}", response_model=Todo) @router.get("/{todo_id}", response_model=Todo)
def read_todo(todo_id: int, db: Session = Depends(get_db)): def read_todo(todo_id: int, db: Session = Depends(get_db)):
"""Get a specific todo by ID"""
db_todo = crud.get_todo(db, todo_id=todo_id) db_todo = crud.get_todo(db, todo_id=todo_id)
if db_todo is None: if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
@ -30,6 +71,7 @@ def read_todo(todo_id: int, db: Session = Depends(get_db)):
@router.put("/{todo_id}", response_model=Todo) @router.put("/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)): def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
"""Update a todo with new values including category, priority and due date"""
db_todo = crud.update_todo(db=db, todo_id=todo_id, todo=todo) db_todo = crud.update_todo(db=db, todo_id=todo_id, todo=todo)
if db_todo is None: if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")
@ -38,6 +80,7 @@ def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(todo_id: int, db: Session = Depends(get_db)): def delete_todo(todo_id: int, db: Session = Depends(get_db)):
"""Delete a todo by ID"""
success = crud.delete_todo(db=db, todo_id=todo_id) success = crud.delete_todo(db=db, todo_id=todo_id)
if not success: if not success:
raise HTTPException(status_code=404, detail="Todo not found") raise HTTPException(status_code=404, detail="Todo not found")

View File

@ -1,7 +1,9 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List, Optional from sqlalchemy import asc, desc
from typing import List, Optional, Dict, Any
from datetime import datetime
from app.models.todo import Todo from app.models.todo import Todo, PriorityEnum
from app.schemas.todo import TodoCreate, TodoUpdate from app.schemas.todo import TodoCreate, TodoUpdate
@ -9,16 +11,58 @@ def get_todo(db: Session, todo_id: int) -> Optional[Todo]:
return db.query(Todo).filter(Todo.id == todo_id).first() return db.query(Todo).filter(Todo.id == todo_id).first()
def get_todos(db: Session, skip: int = 0, limit: int = 100) -> List[Todo]: def get_todos(
return db.query(Todo).order_by(Todo.created_at.desc()).offset(skip).limit(limit).all() db: Session,
skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
priority: Optional[str] = None,
completed: Optional[bool] = None,
due_before: Optional[datetime] = None,
due_after: Optional[datetime] = None,
sort_by: str = "created_at",
sort_desc: bool = True
) -> List[Todo]:
query = db.query(Todo)
# Apply filters if provided
if category:
query = query.filter(Todo.category == category)
if priority:
try:
priority_enum = PriorityEnum[priority.upper()]
query = query.filter(Todo.priority == priority_enum)
except (KeyError, AttributeError):
pass # Invalid priority value, ignore filter
if completed is not None:
query = query.filter(Todo.completed == completed)
if due_before:
query = query.filter(Todo.due_date <= due_before)
if due_after:
query = query.filter(Todo.due_date >= due_after)
# Apply sorting
if hasattr(Todo, sort_by):
sort_column = getattr(Todo, sort_by)
if sort_desc:
query = query.order_by(desc(sort_column))
else:
query = query.order_by(asc(sort_column))
else:
# Default sort by created_at if invalid column provided
query = query.order_by(desc(Todo.created_at))
# Apply pagination
return query.offset(skip).limit(limit).all()
def create_todo(db: Session, todo: TodoCreate) -> Todo: def create_todo(db: Session, todo: TodoCreate) -> Todo:
db_todo = Todo( todo_data = todo.model_dump()
title=todo.title, db_todo = Todo(**todo_data)
description=todo.description,
completed=todo.completed
)
db.add(db_todo) db.add(db_todo)
db.commit() db.commit()
db.refresh(db_todo) db.refresh(db_todo)
@ -47,3 +91,42 @@ def delete_todo(db: Session, todo_id: int) -> bool:
db.delete(db_todo) db.delete(db_todo)
db.commit() db.commit()
return True return True
def get_todo_stats(db: Session) -> Dict[str, Any]:
"""Get statistics about todos"""
total = db.query(Todo).count()
completed = db.query(Todo).filter(Todo.completed == True).count()
incomplete = total - completed
# Group by category
categories = db.query(
Todo.category,
db.func.count(Todo.id)
).group_by(
Todo.category
).all()
# Group by priority
priorities = db.query(
Todo.priority,
db.func.count(Todo.id)
).group_by(
Todo.priority
).all()
# Overdue todos (due date in past and not completed)
now = datetime.now()
overdue = db.query(Todo).filter(
Todo.due_date < now,
Todo.completed == False
).count()
return {
"total": total,
"completed": completed,
"incomplete": incomplete,
"overdue": overdue,
"by_category": {cat or "uncategorized": count for cat, count in categories},
"by_priority": {str(pri.name).lower() if pri else "none": count for pri, count in priorities}
}

View File

@ -1,8 +1,14 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum
from sqlalchemy.sql import func from sqlalchemy.sql import func
import enum
from app.db.base import Base from app.db.base import Base
class PriorityEnum(enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Todo(Base): class Todo(Base):
__tablename__ = "todos" __tablename__ = "todos"
@ -10,5 +16,8 @@ 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)
priority = Column(Enum(PriorityEnum), default=PriorityEnum.MEDIUM, nullable=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())

View File

@ -1,11 +1,20 @@
from pydantic import BaseModel from pydantic import BaseModel, Field
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional, Literal
from enum import Enum
class PriorityEnum(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class TodoBase(BaseModel): 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
priority: Optional[PriorityEnum] = PriorityEnum.MEDIUM
due_date: Optional[datetime] = None
class TodoCreate(TodoBase): class TodoCreate(TodoBase):
pass pass
@ -14,6 +23,9 @@ 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
priority: Optional[PriorityEnum] = None
due_date: Optional[datetime] = None
class TodoInDB(TodoBase): class TodoInDB(TodoBase):
id: int id: int