From fe3924497e8afc6bf49184cb6859f7997d275f57 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Wed, 14 May 2025 01:27:39 +0000 Subject: [PATCH] Add todo due date and priority features - Added priority (low, medium, high) to todo items - Added due date to todo items - Enhanced API to support filtering by priority and due date - Added overdue and due_soon filters for better task management - Automatic sorting by priority and due date - Created alembic migration for the new fields - Updated documentation generated with BackendIM... (backend.im) --- README.md | 19 ++++- alembic/env.py | 1 - .../add_due_date_and_priority_fields.py | 35 ++++++++++ app/api/endpoints/todos.py | 69 ++++++++++++++++--- app/models/todo.py | 21 +++++- app/schemas/todo.py | 11 +++ 6 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 alembic/versions/add_due_date_and_priority_fields.py diff --git a/README.md b/README.md index f3fb354..fd13b74 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,11 @@ A simple Todo application built with FastAPI and SQLite. ## Features - Create, read, update, and delete todos -- Filter todos by completion status +- Assign priority levels (low, medium, high) to todos +- Set due dates for todos +- Filter todos by completion status, priority, and due dates +- Find overdue and soon-to-be-due todos +- Automatic sorting by priority and due date - Health endpoint for application monitoring ## Project Structure @@ -54,9 +58,18 @@ uvicorn main:app --reload ### Todos - `GET /api/todos`: List all todos -- `POST /api/todos`: Create a new todo + - Query parameters: + - `completed`: Filter by completion status (boolean) + - `priority`: Filter by priority level (low, medium, high) + - `due_date_before`: Filter todos due before this date + - `due_date_after`: Filter todos due after this date + - `due_soon`: Get todos due within the next 3 days (boolean) + - `overdue`: Get overdue todos (boolean) + - `skip`: Number of records to skip (pagination) + - `limit`: Maximum number of records to return (pagination) +- `POST /api/todos`: Create a new todo (includes priority and due date) - `GET /api/todos/{todo_id}`: Get a specific todo -- `PUT /api/todos/{todo_id}`: Update a todo +- `PUT /api/todos/{todo_id}`: Update a todo (can update priority and due date) - `DELETE /api/todos/{todo_id}`: Delete a todo ### Health diff --git a/alembic/env.py b/alembic/env.py index 22f2ae6..82c4144 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -8,7 +8,6 @@ import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.db.base import Base -from app.models.todo import Todo # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/alembic/versions/add_due_date_and_priority_fields.py b/alembic/versions/add_due_date_and_priority_fields.py new file mode 100644 index 0000000..b00e7a5 --- /dev/null +++ b/alembic/versions/add_due_date_and_priority_fields.py @@ -0,0 +1,35 @@ +"""Add due_date and priority fields to todo + +Revision ID: add_due_date_and_priority +Revises: initial_migration +Create Date: 2025-05-14 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'add_due_date_and_priority' +down_revision = 'initial_migration' +branch_labels = None +depends_on = None + + +def upgrade(): + # SQLite doesn't support ENUM types natively, so we'll use a string with a CHECK constraint + with op.batch_alter_table('todos') as batch_op: + batch_op.add_column(sa.Column('priority', sa.String(10), nullable=False, server_default='medium')) + batch_op.add_column(sa.Column('due_date', sa.DateTime(timezone=True), nullable=True)) + # Add a check constraint to ensure priority is one of the allowed values + batch_op.create_check_constraint( + 'priority_check', + "priority IN ('low', 'medium', 'high')" + ) + + +def downgrade(): + with op.batch_alter_table('todos') as batch_op: + batch_op.drop_constraint('priority_check') + batch_op.drop_column('due_date') + batch_op.drop_column('priority') \ No newline at end of file diff --git a/app/api/endpoints/todos.py b/app/api/endpoints/todos.py index aa78b53..e7d8a6f 100644 --- a/app/api/endpoints/todos.py +++ b/app/api/endpoints/todos.py @@ -1,10 +1,12 @@ from typing import List, Optional -from fastapi import APIRouter, Depends, HTTPException, Query, status +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session +from sqlalchemy import and_, not_ from app.db.session import get_db from app.models.todo import Todo -from app.schemas.todo import TodoCreate, TodoUpdate, Todo as TodoSchema +from app.schemas.todo import TodoCreate, TodoUpdate, Todo as TodoSchema, PriorityEnum as SchemaPriorityEnum router = APIRouter( prefix="/todos", @@ -24,14 +26,65 @@ def create_todo(todo: TodoCreate, db: Session = Depends(get_db)): @router.get("/", response_model=List[TodoSchema]) def read_todos( - skip: int = 0, - limit: int = 100, + skip: int = 0, + limit: int = 100, completed: Optional[bool] = None, + priority: Optional[SchemaPriorityEnum] = None, + due_date_before: Optional[datetime] = None, + due_date_after: Optional[datetime] = None, + due_soon: Optional[bool] = None, + overdue: Optional[bool] = None, db: Session = Depends(get_db) ): query = db.query(Todo) + filters = [] + + # Apply completed filter if completed is not None: - query = query.filter(Todo.completed == completed) + filters.append(Todo.completed == completed) + + # Apply priority filter + if priority is not None: + filters.append(Todo.priority == priority.value) + + # Apply due date filters + if due_date_before is not None: + filters.append(Todo.due_date <= due_date_before) + + if due_date_after is not None: + filters.append(Todo.due_date >= due_date_after) + + # Due soon filter (next 3 days) + if due_soon is True: + now = datetime.now() + three_days_later = now + timedelta(days=3) + filters.append(and_( + Todo.due_date >= now, + Todo.due_date <= three_days_later, + not_(Todo.completed) + )) + + # Overdue filter + if overdue is True: + now = datetime.now() + filters.append(and_( + Todo.due_date < now, + not_(Todo.completed) + )) + + # Apply all filters + if filters: + query = query.filter(and_(*filters)) + + # Order by priority (high to low) and due date (closest first) + query = query.order_by( + # Custom ordering for priority + # This works because of our string values 'high', 'medium', 'low' + Todo.priority.desc(), + # Due date, null values last + Todo.due_date.asc() + ) + todos = query.offset(skip).limit(limit).all() return todos @@ -49,11 +102,11 @@ def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)): db_todo = db.query(Todo).filter(Todo.id == todo_id).first() if db_todo is None: raise HTTPException(status_code=404, detail="Todo not found") - + update_data = todo.dict(exclude_unset=True) for key, value in update_data.items(): setattr(db_todo, key, value) - + db.commit() db.refresh(db_todo) return db_todo @@ -64,7 +117,7 @@ def delete_todo(todo_id: int, db: Session = Depends(get_db)): db_todo = db.query(Todo).filter(Todo.id == todo_id).first() if db_todo is None: raise HTTPException(status_code=404, detail="Todo not found") - + db.delete(db_todo) db.commit() return None \ No newline at end of file diff --git a/app/models/todo.py b/app/models/todo.py index 13b1203..2f3e14e 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -1,6 +1,13 @@ -from sqlalchemy import Boolean, Column, Integer, String, DateTime +from sqlalchemy import Boolean, Column, Integer, String, DateTime, CheckConstraint from sqlalchemy.sql import func from app.db.base import Base +import enum + + +class PriorityEnum(str, enum.Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" class Todo(Base): @@ -10,5 +17,15 @@ class Todo(Base): title = Column(String, index=True) description = Column(String, nullable=True) completed = Column(Boolean, default=False) + priority = Column(String(10), default=PriorityEnum.MEDIUM.value) + 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()) \ No newline at end of file + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + # Add a check constraint to ensure priority is one of the allowed values + __table_args__ = ( + CheckConstraint( + "priority IN ('low', 'medium', 'high')", + name="priority_check" + ), + ) \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py index 5cb0ca6..619eee4 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -1,12 +1,21 @@ from datetime import datetime from typing import Optional from pydantic import BaseModel +from enum import Enum + + +class PriorityEnum(str, Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" class TodoBase(BaseModel): title: str description: Optional[str] = None completed: bool = False + priority: PriorityEnum = PriorityEnum.MEDIUM + due_date: Optional[datetime] = None class TodoCreate(TodoBase): @@ -17,6 +26,8 @@ class TodoUpdate(BaseModel): title: Optional[str] = None description: Optional[str] = None completed: Optional[bool] = None + priority: Optional[PriorityEnum] = None + due_date: Optional[datetime] = None class TodoInDB(TodoBase):