From 091c42482c3bdcd5c4ef5b04eb0c7dbe9b5c4552 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Tue, 17 Jun 2025 05:51:35 +0000 Subject: [PATCH] Add todo categories and due dates with filtering capabilities - Added category field to Todo model for organizing todos - Added due_date field with timezone support for deadline tracking - Enhanced CRUD operations with filtering by category, completion status, and overdue items - Added new API endpoints for category-based and overdue todo retrieval - Updated API documentation with new filtering query parameters - Created database migration for new fields with proper indexing - Updated README with comprehensive feature documentation --- README.md | 10 +++- app/api/v1/todos.py | 40 ++++++++++++++-- app/crud/todo.py | 46 +++++++++++++++++-- app/models/todo.py | 2 + app/schemas/todo.py | 4 ++ .../versions/002_add_category_and_due_date.py | 35 ++++++++++++++ 6 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/002_add_category_and_due_date.py diff --git a/README.md b/README.md index 5744eea..75e0bb6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ A simple Todo application API built with FastAPI and SQLite. ## Features - ✅ Create, read, update, and delete todos +- ✅ Todo categories/tags for organization +- ✅ Due dates with overdue detection +- ✅ Advanced filtering (by category, completion status, overdue) - ✅ SQLite database with SQLAlchemy ORM - ✅ Database migrations with Alembic - ✅ API documentation with Swagger UI @@ -54,11 +57,14 @@ The API is available at `http://localhost:8000` - **GET** `/health` - Health check endpoint ### Todo Endpoints -- **GET** `/api/v1/todos/` - List all todos (with pagination) +- **GET** `/api/v1/todos/` - List all todos (with pagination and filtering) + - Query parameters: `skip`, `limit`, `category`, `completed`, `overdue_only` - **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 specific todo - **DELETE** `/api/v1/todos/{todo_id}` - Delete a specific todo +- **GET** `/api/v1/todos/categories/{category}` - Get all todos by category +- **GET** `/api/v1/todos/overdue` - Get all overdue todos ## Database @@ -71,6 +77,8 @@ The application uses SQLite database stored at `/app/storage/db/db.sqlite`. - `title` (String, Required) - `description` (String, Optional) - `completed` (Boolean, Default: False) +- `category` (String, Optional) - For organizing todos +- `due_date` (DateTime with timezone, Optional) - When the todo is due - `created_at` (DateTime with timezone) - `updated_at` (DateTime with timezone) diff --git a/app/api/v1/todos.py b/app/api/v1/todos.py index 6994b0f..b6b88f2 100644 --- a/app/api/v1/todos.py +++ b/app/api/v1/todos.py @@ -1,6 +1,6 @@ -from typing import List +from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from app.crud import todo @@ -14,15 +14,47 @@ router = APIRouter() def read_todos( skip: int = 0, limit: int = 100, + category: Optional[str] = Query(None, description="Filter by category"), + completed: Optional[bool] = Query(None, description="Filter by completion status"), + overdue_only: bool = Query(False, description="Show only overdue todos"), db: Session = Depends(get_db), ): """ - Retrieve todos with pagination. + Retrieve todos with pagination and filtering. """ - todos = todo.get_multi(db, skip=skip, limit=limit) + todos = todo.get_multi( + db, + skip=skip, + limit=limit, + category=category, + completed=completed, + overdue_only=overdue_only, + ) return todos +@router.get("/categories/{category}", response_model=List[TodoResponse]) +def read_todos_by_category( + *, + db: Session = Depends(get_db), + category: str, +): + """ + Get all todos by category. + """ + return todo.get_by_category(db=db, category=category) + + +@router.get("/overdue", response_model=List[TodoResponse]) +def read_overdue_todos( + db: Session = Depends(get_db), +): + """ + Get all overdue todos. + """ + return todo.get_overdue(db=db) + + @router.post("/", response_model=TodoResponse, status_code=status.HTTP_201_CREATED) def create_todo( *, diff --git a/app/crud/todo.py b/app/crud/todo.py index e83d012..c3e70c7 100644 --- a/app/crud/todo.py +++ b/app/crud/todo.py @@ -1,6 +1,8 @@ from typing import List, Optional +from datetime import datetime from sqlalchemy.orm import Session +from sqlalchemy import and_, not_ from app.models.todo import Todo from app.schemas.todo import TodoCreate, TodoUpdate @@ -13,9 +15,33 @@ class CRUDTodo: """Get a single todo by ID.""" return db.query(Todo).filter(Todo.id == todo_id).first() - def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Todo]: - """Get multiple todos with pagination.""" - return db.query(Todo).offset(skip).limit(limit).all() + def get_multi( + self, + db: Session, + *, + skip: int = 0, + limit: int = 100, + category: Optional[str] = None, + completed: Optional[bool] = None, + overdue_only: bool = False, + ) -> List[Todo]: + """Get multiple todos with pagination and filtering.""" + query = db.query(Todo) + + filters = [] + if category: + filters.append(Todo.category == category) + if completed is not None: + filters.append(Todo.completed == completed) + if overdue_only: + filters.append( + and_(Todo.due_date < datetime.now(), not_(Todo.completed)) + ) + + if filters: + query = query.filter(and_(*filters)) + + return query.offset(skip).limit(limit).all() def create(self, db: Session, *, obj_in: TodoCreate) -> Todo: """Create a new todo.""" @@ -23,6 +49,8 @@ class CRUDTodo: title=obj_in.title, description=obj_in.description, completed=obj_in.completed, + category=obj_in.category, + due_date=obj_in.due_date, ) db.add(db_obj) db.commit() @@ -47,5 +75,17 @@ class CRUDTodo: db.commit() return obj + def get_by_category(self, db: Session, *, category: str) -> List[Todo]: + """Get all todos by category.""" + return db.query(Todo).filter(Todo.category == category).all() + + def get_overdue(self, db: Session) -> List[Todo]: + """Get all overdue todos.""" + return ( + db.query(Todo) + .filter(and_(Todo.due_date < datetime.now(), not_(Todo.completed))) + .all() + ) + todo = CRUDTodo() diff --git a/app/models/todo.py b/app/models/todo.py index 55e38ce..09a66d6 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -13,5 +13,7 @@ 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) + 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()) diff --git a/app/schemas/todo.py b/app/schemas/todo.py index bb0be9a..0260fb7 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -10,6 +10,8 @@ class TodoBase(BaseModel): title: str description: Optional[str] = None completed: bool = False + category: Optional[str] = None + due_date: Optional[datetime] = None class TodoCreate(TodoBase): @@ -24,6 +26,8 @@ class TodoUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None completed: Optional[bool] = None + category: Optional[str] = None + due_date: Optional[datetime] = None class TodoResponse(TodoBase): diff --git a/migrations/versions/002_add_category_and_due_date.py b/migrations/versions/002_add_category_and_due_date.py new file mode 100644 index 0000000..4e38cd3 --- /dev/null +++ b/migrations/versions/002_add_category_and_due_date.py @@ -0,0 +1,35 @@ +"""Add category and due_date to todos table + +Revision ID: 002 +Revises: 001 +Create Date: 2025-06-17 12: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(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("todos", sa.Column("category", sa.String(), nullable=True)) + op.add_column( + "todos", sa.Column("due_date", sa.DateTime(timezone=True), nullable=True) + ) + op.create_index(op.f("ix_todos_category"), "todos", ["category"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_todos_category"), table_name="todos") + op.drop_column("todos", "due_date") + op.drop_column("todos", "category") + # ### end Alembic commands ###