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 typing import Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
import math
|
||||||
|
|
||||||
from app.crud import todo as todo_crud
|
from app.crud import todo as todo_crud
|
||||||
from app.db.session import get_db
|
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 = APIRouter(prefix="/todos", tags=["todos"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=List[Todo])
|
@router.get("/", response_model=TodoListResponse)
|
||||||
def read_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
def read_todos(
|
||||||
todos = todo_crud.get_todos(db, skip=skip, limit=limit)
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
return todos
|
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)
|
@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 sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.todo import Todo
|
from app.models.todo import Todo, Priority
|
||||||
from app.schemas.todo import TodoCreate, TodoUpdate
|
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()
|
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).offset(skip).limit(limit).all()
|
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:
|
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
|
from sqlalchemy.sql import func
|
||||||
|
import enum
|
||||||
|
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Priority(str, enum.Enum):
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
|
||||||
|
|
||||||
class Todo(Base):
|
class Todo(Base):
|
||||||
__tablename__ = "todos"
|
__tablename__ = "todos"
|
||||||
|
|
||||||
@ -11,5 +18,6 @@ class Todo(Base):
|
|||||||
title = Column(String(200), nullable=False)
|
title = Column(String(200), nullable=False)
|
||||||
description = Column(String(500), nullable=True)
|
description = Column(String(500), nullable=True)
|
||||||
completed = Column(Boolean, default=False)
|
completed = Column(Boolean, default=False)
|
||||||
|
priority = Column(Enum(Priority), default=Priority.MEDIUM)
|
||||||
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())
|
||||||
|
@ -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 typing import Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.models.todo import Priority
|
||||||
|
|
||||||
|
|
||||||
class TodoBase(BaseModel):
|
class TodoBase(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
completed: bool = False
|
completed: bool = False
|
||||||
|
priority: Priority = Priority.MEDIUM
|
||||||
|
|
||||||
|
|
||||||
class TodoCreate(TodoBase):
|
class TodoCreate(TodoBase):
|
||||||
@ -17,6 +20,7 @@ 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
|
||||||
|
priority: Optional[Priority] = None
|
||||||
|
|
||||||
|
|
||||||
class Todo(TodoBase):
|
class Todo(TodoBase):
|
||||||
@ -26,3 +30,12 @@ class Todo(TodoBase):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
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