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
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
- RESTful API for Todo management
- 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
- Alembic for database migrations
- FastAPI's automatic OpenAPI documentation
@ -38,15 +48,35 @@ simpletodoapplication/
## 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
- **GET /api/v1/todos/{todo_id}** - Get a specific todo
- **PUT /api/v1/todos/{todo_id}** - Update 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 /docs** - API documentation (Swagger UI)
- **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
1. Install dependencies:
@ -77,7 +107,34 @@ simpletodoapplication/
"title": "Sample Todo",
"description": "This is a sample todo item",
"completed": false,
"category": "work",
"priority": "high",
"due_date": "2025-05-20T12:00:00",
"created_at": "2025-05-13T12:00:00",
"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 typing import List
from typing import List, Optional, Dict, Any
from datetime import datetime
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
router = APIRouter()
@router.get("/", response_model=List[Todo])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
todos = crud.get_todos(db, skip=skip, limit=limit)
def read_todos(
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
@router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED)
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)
@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)
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)
if db_todo is None:
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)
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)
if db_todo is None:
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)
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)
if not success:
raise HTTPException(status_code=404, detail="Todo not found")

View File

@ -1,7 +1,9 @@
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
@ -9,16 +11,58 @@ def get_todo(db: Session, todo_id: int) -> Optional[Todo]:
return db.query(Todo).filter(Todo.id == todo_id).first()
def get_todos(db: Session, skip: int = 0, limit: int = 100) -> List[Todo]:
return db.query(Todo).order_by(Todo.created_at.desc()).offset(skip).limit(limit).all()
def get_todos(
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:
db_todo = Todo(
title=todo.title,
description=todo.description,
completed=todo.completed
)
todo_data = todo.model_dump()
db_todo = Todo(**todo_data)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
@ -29,11 +73,11 @@ def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
db_todo = get_todo(db, todo_id)
if db_todo is None:
return None
update_data = todo.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(db_todo, field, value)
db.commit()
db.refresh(db_todo)
return db_todo
@ -43,7 +87,46 @@ def delete_todo(db: Session, todo_id: int) -> bool:
db_todo = get_todo(db, todo_id)
if db_todo is None:
return False
db.delete(db_todo)
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
import enum
from app.db.base import Base
class PriorityEnum(enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Todo(Base):
__tablename__ = "todos"
@ -10,5 +16,8 @@ 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)
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())
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 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):
title: str
description: Optional[str] = None
completed: bool = False
category: Optional[str] = None
priority: Optional[PriorityEnum] = PriorityEnum.MEDIUM
due_date: Optional[datetime] = None
class TodoCreate(TodoBase):
pass
@ -14,6 +23,9 @@ class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
category: Optional[str] = None
priority: Optional[PriorityEnum] = None
due_date: Optional[datetime] = None
class TodoInDB(TodoBase):
id: int