Add priority levels, enhanced pagination, and search/filtering

- Add Priority enum (low, medium, high) to Todo model with default medium
- Create migration to add priority field to existing todos table
- Enhance pagination with proper metadata (page, total, has_next, has_prev)
- Add search functionality across title and description fields
- Add filtering by completion status and priority level
- Update API endpoints with query parameters for filtering and search
- Add TodoListResponse schema for structured pagination response
- Format code with Ruff
This commit is contained in:
Automated Action 2025-06-18 01:01:29 +00:00
parent 2c24afcb6c
commit 19ab35d8f5
6 changed files with 124 additions and 14 deletions

View File

@ -0,0 +1,37 @@
"""Add priority field to todos
Revision ID: 002
Revises: 001
Create Date: 2024-01-01 00: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() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"todos",
sa.Column(
"priority",
sa.Enum("LOW", "MEDIUM", "HIGH", name="priority"),
nullable=True,
),
)
# Set default value for existing records
op.execute("UPDATE todos SET priority = 'MEDIUM' WHERE priority IS NULL")
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("todos", "priority")
# ### end Alembic commands ###

View File

@ -1,18 +1,45 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
import math
from app.crud import todo as todo_crud
from app.db.session import get_db
from app.schemas.todo import Todo, TodoCreate, TodoUpdate
from app.models.todo import Priority
from app.schemas.todo import Todo, TodoCreate, TodoUpdate, TodoListResponse
router = APIRouter(prefix="/todos", tags=["todos"])
@router.get("/", response_model=List[Todo])
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
todos = todo_crud.get_todos(db, skip=skip, limit=limit)
return todos
@router.get("/", response_model=TodoListResponse)
def read_todos(
page: int = Query(1, ge=1, description="Page number"),
per_page: int = Query(10, ge=1, le=100, description="Items per page"),
completed: Optional[bool] = Query(None, description="Filter by completion status"),
priority: Optional[Priority] = Query(None, description="Filter by priority"),
search: Optional[str] = Query(None, description="Search in title and description"),
db: Session = Depends(get_db),
):
skip = (page - 1) * per_page
todos, total = todo_crud.get_todos(
db,
skip=skip,
limit=per_page,
completed=completed,
priority=priority,
search=search,
)
total_pages = math.ceil(total / per_page)
return TodoListResponse(
items=todos,
total=total,
page=page,
per_page=per_page,
has_next=page < total_pages,
has_prev=page > 1,
)
@router.post("/", response_model=Todo)

View File

@ -1,7 +1,7 @@
from typing import List, Optional
from typing import List, Optional, Tuple
from sqlalchemy.orm import Session
from app.models.todo import Todo
from app.models.todo import Todo, Priority
from app.schemas.todo import TodoCreate, TodoUpdate
@ -9,8 +9,33 @@ 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).offset(skip).limit(limit).all()
def get_todos(
db: Session,
skip: int = 0,
limit: int = 100,
completed: Optional[bool] = None,
priority: Optional[Priority] = None,
search: Optional[str] = None,
) -> Tuple[List[Todo], int]:
query = db.query(Todo)
# Apply filters
if completed is not None:
query = query.filter(Todo.completed == completed)
if priority is not None:
query = query.filter(Todo.priority == priority)
if search:
query = query.filter(
Todo.title.contains(search) | Todo.description.contains(search)
)
# Get total count before pagination
total = query.count()
# Apply pagination and ordering
todos = query.order_by(Todo.created_at.desc()).offset(skip).limit(limit).all()
return todos, total
def create_todo(db: Session, todo: TodoCreate) -> Todo:

View File

@ -1,9 +1,16 @@
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 Priority(str, enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Todo(Base):
__tablename__ = "todos"
@ -11,5 +18,6 @@ class Todo(Base):
title = Column(String(200), nullable=False)
description = Column(String(500), nullable=True)
completed = Column(Boolean, default=False)
priority = Column(Enum(Priority), default=Priority.MEDIUM)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

View File

@ -1,3 +1,3 @@
from .todo import Todo, TodoCreate, TodoUpdate
from .todo import Todo, TodoCreate, TodoUpdate, TodoListResponse
__all__ = ["Todo", "TodoCreate", "TodoUpdate"]
__all__ = ["Todo", "TodoCreate", "TodoUpdate", "TodoListResponse"]

View File

@ -2,11 +2,14 @@ from datetime import datetime
from typing import Optional
from pydantic import BaseModel
from app.models.todo import Priority
class TodoBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
priority: Priority = Priority.MEDIUM
class TodoCreate(TodoBase):
@ -17,6 +20,7 @@ class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
priority: Optional[Priority] = None
class Todo(TodoBase):
@ -26,3 +30,12 @@ class Todo(TodoBase):
class Config:
from_attributes = True
class TodoListResponse(BaseModel):
items: list[Todo]
total: int
page: int
per_page: int
has_next: bool
has_prev: bool