From 19ab35d8f58e70db9ed3d2d49829156a793b303a Mon Sep 17 00:00:00 2001 From: Automated Action Date: Wed, 18 Jun 2025 01:01:29 +0000 Subject: [PATCH] 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 --- alembic/versions/002_add_priority_field.py | 37 +++++++++++++++++++ app/api/v1/todos.py | 41 ++++++++++++++++++---- app/crud/todo.py | 33 ++++++++++++++--- app/models/todo.py | 10 +++++- app/schemas/__init__.py | 4 +-- app/schemas/todo.py | 13 +++++++ 6 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 alembic/versions/002_add_priority_field.py diff --git a/alembic/versions/002_add_priority_field.py b/alembic/versions/002_add_priority_field.py new file mode 100644 index 0000000..b81e133 --- /dev/null +++ b/alembic/versions/002_add_priority_field.py @@ -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 ### diff --git a/app/api/v1/todos.py b/app/api/v1/todos.py index 07ab1e8..fba7f76 100644 --- a/app/api/v1/todos.py +++ b/app/api/v1/todos.py @@ -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) diff --git a/app/crud/todo.py b/app/crud/todo.py index 71bfd97..7aa26f6 100644 --- a/app/crud/todo.py +++ b/app/crud/todo.py @@ -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: diff --git a/app/models/todo.py b/app/models/todo.py index e0dad85..ceb03b9 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -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()) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 08ef510..50b0eb8 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -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"] diff --git a/app/schemas/todo.py b/app/schemas/todo.py index 269edc7..8237489 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -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