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:
parent
2c24afcb6c
commit
19ab35d8f5
37
alembic/versions/002_add_priority_field.py
Normal file
37
alembic/versions/002_add_priority_field.py
Normal 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 ###
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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())
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user