From 660c28b96c320a7f696c8039e7dc86c7ea040757 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Mon, 12 May 2025 23:37:35 +0000 Subject: [PATCH] Add subtasks and reminders functionality - Added subtask model with CRUD operations - Added reminder functionality to Todo model - Added endpoints for managing subtasks and reminders - Created new migration for subtasks and reminders - Updated documentation generated with BackendIM... (backend.im) --- README.md | 17 ++- app/models.py | 21 +++- app/schemas.py | 23 ++++ main.py | 100 +++++++++++++++++- .../004_add_subtasks_and_reminders.py | 46 ++++++++ 5 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/004_add_subtasks_and_reminders.py diff --git a/README.md b/README.md index 37f10f4..ad248fc 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ This is an improved Todo API application built with FastAPI and SQLite, featurin - Health endpoint for monitoring application status - SQLite database for data persistence - Alembic for database migrations +- **NEW** Subtasks support for breaking down complex todos +- **NEW** Task reminder functionality ## Project Structure @@ -27,7 +29,8 @@ simpletodoapplication/ │ ├── versions/ │ │ ├── 001_create_todos_table.py │ │ ├── 002_add_priority_and_due_date.py -│ │ └── 003_add_tags_table_and_associations.py +│ │ ├── 003_add_tags_table_and_associations.py +│ │ └── 004_add_subtasks_and_reminders.py │ ├── env.py │ ├── README │ └── script.py.mako @@ -52,6 +55,7 @@ simpletodoapplication/ ### Search & Filtering - `GET /todos/search` - Search todos by title, description, or tag - `GET /todos/upcoming` - Get todos due in the next N days (defaults to 7) +- `GET /todos/reminders` - Get todos with reminders in the next N hours (defaults to 24) - `GET /todos` - With query parameters for advanced filtering: - `title` - Filter by title (partial match) - `description` - Filter by description (partial match) @@ -67,6 +71,17 @@ simpletodoapplication/ - `POST /tags` - Create a new tag - `GET /tags/{tag_id}` - Get a specific tag +### Subtask Management +- `POST /todos/{todo_id}/subtasks` - Create a new subtask for a todo +- `GET /todos/{todo_id}/subtasks` - Get all subtasks for a todo +- `GET /subtasks/{subtask_id}` - Get a specific subtask +- `PUT /subtasks/{subtask_id}` - Update a subtask +- `DELETE /subtasks/{subtask_id}` - Delete a subtask +- `PUT /todos/{todo_id}/complete-all-subtasks` - Mark all subtasks of a todo as completed + +### Reminder Management +- `PUT /todos/{todo_id}/set-reminder` - Set or update a reminder for a todo + ## Requirements - Python 3.8+ diff --git a/app/models.py b/app/models.py index 160756f..faa0f3e 100644 --- a/app/models.py +++ b/app/models.py @@ -28,6 +28,21 @@ class Tag(Base): # Relationship to todos todos = relationship("Todo", secondary=todo_tag, back_populates="tags") +class Subtask(Base): + __tablename__ = "subtasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(100), nullable=False) + completed = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + # Foreign key relationship + todo_id = Column(Integer, ForeignKey("todos.id", ondelete="CASCADE"), nullable=False) + + # Relationship to parent todo + todo = relationship("Todo", back_populates="subtasks") + class Todo(Base): __tablename__ = "todos" @@ -37,8 +52,12 @@ class Todo(Base): completed = Column(Boolean, default=False) priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM) due_date = Column(DateTime(timezone=True), nullable=True) + remind_at = 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()) # Relationship to tags - tags = relationship("Tag", secondary=todo_tag, back_populates="todos") \ No newline at end of file + tags = relationship("Tag", secondary=todo_tag, back_populates="todos") + + # Relationship to subtasks + subtasks = relationship("Subtask", back_populates="todo", cascade="all, delete-orphan") \ No newline at end of file diff --git a/app/schemas.py b/app/schemas.py index 2709d1d..b8c7e4d 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -21,12 +21,33 @@ class TagResponse(TagBase): class Config: from_attributes = True +class SubtaskBase(BaseModel): + title: str = Field(..., min_length=1, max_length=100) + completed: bool = False + +class SubtaskCreate(SubtaskBase): + pass + +class SubtaskUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=100) + completed: Optional[bool] = None + +class SubtaskResponse(SubtaskBase): + id: int + todo_id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + class TodoBase(BaseModel): title: str = Field(..., min_length=1, max_length=100) description: Optional[str] = None completed: bool = False priority: PriorityLevel = PriorityLevel.MEDIUM due_date: Optional[datetime] = None + remind_at: Optional[datetime] = None class TodoCreate(TodoBase): tag_ids: Optional[List[int]] = [] @@ -40,6 +61,7 @@ class TodoUpdate(BaseModel): completed: Optional[bool] = None priority: Optional[PriorityLevel] = None due_date: Optional[datetime] = None + remind_at: Optional[datetime] = None tag_ids: Optional[List[int]] = None class TodoResponse(TodoBase): @@ -47,6 +69,7 @@ class TodoResponse(TodoBase): created_at: datetime updated_at: Optional[datetime] = None tags: List[TagResponse] = [] + subtasks: List[SubtaskResponse] = [] class Config: from_attributes = True diff --git a/main.py b/main.py index 75c30b9..0b0df42 100644 --- a/main.py +++ b/main.py @@ -7,10 +7,11 @@ from sqlalchemy import or_, and_, func from datetime import datetime, timedelta from app.database import get_db, engine -from app.models import Todo, Tag, Base +from app.models import Todo, Tag, Subtask, Base from app.schemas import ( TodoCreate, TodoCreateWithTags, TodoUpdate, TodoResponse, - TagCreate, TagResponse, HealthResponse + TagCreate, TagResponse, HealthResponse, + SubtaskCreate, SubtaskUpdate, SubtaskResponse ) # Create tables if they don't exist @@ -165,6 +166,32 @@ def get_upcoming_todos(days: int = 7, db: Session = Depends(get_db)): ) ).order_by(Todo.due_date).all() +@app.get("/todos/reminders/", response_model=List[TodoResponse]) +def get_upcoming_reminders(hours: int = 24, db: Session = Depends(get_db)): + """Get todos with reminders in the next N hours""" + now = datetime.now() + future_time = now + timedelta(hours=hours) + + return db.query(Todo).filter( + and_( + Todo.remind_at <= future_time, + Todo.remind_at >= now, + Todo.completed == False + ) + ).order_by(Todo.remind_at).all() + +@app.put("/todos/{todo_id}/set-reminder", response_model=TodoResponse) +def set_reminder(todo_id: int, remind_at: datetime, db: Session = Depends(get_db)): + """Set or update a reminder for a specific todo""" + 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_todo.remind_at = remind_at + db.commit() + db.refresh(db_todo) + return db_todo + @app.get("/todos/{todo_id}", response_model=TodoResponse) def get_todo(todo_id: int, db: Session = Depends(get_db)): db_todo = db.query(Todo).filter(Todo.id == todo_id).first() @@ -208,5 +235,74 @@ def delete_todo(todo_id: int, db: Session = Depends(get_db)): db.commit() return db_todo +# Subtasks endpoints +@app.post("/todos/{todo_id}/subtasks/", response_model=SubtaskResponse) +def create_subtask(todo_id: int, subtask: SubtaskCreate, db: Session = Depends(get_db)): + """Create a new subtask for a specific todo""" + 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_subtask = Subtask(**subtask.model_dump(), todo_id=todo_id) + db.add(db_subtask) + db.commit() + db.refresh(db_subtask) + return db_subtask + +@app.get("/todos/{todo_id}/subtasks/", response_model=List[SubtaskResponse]) +def get_subtasks(todo_id: int, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + """Get all subtasks for a todo""" + 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") + + return db.query(Subtask).filter(Subtask.todo_id == todo_id).offset(skip).limit(limit).all() + +@app.get("/subtasks/{subtask_id}", response_model=SubtaskResponse) +def get_subtask(subtask_id: int, db: Session = Depends(get_db)): + """Get a specific subtask by ID""" + db_subtask = db.query(Subtask).filter(Subtask.id == subtask_id).first() + if db_subtask is None: + raise HTTPException(status_code=404, detail="Subtask not found") + return db_subtask + +@app.put("/subtasks/{subtask_id}", response_model=SubtaskResponse) +def update_subtask(subtask_id: int, subtask: SubtaskUpdate, db: Session = Depends(get_db)): + """Update a specific subtask""" + db_subtask = db.query(Subtask).filter(Subtask.id == subtask_id).first() + if db_subtask is None: + raise HTTPException(status_code=404, detail="Subtask not found") + + subtask_data = subtask.model_dump(exclude_unset=True) + for key, value in subtask_data.items(): + setattr(db_subtask, key, value) + + db.commit() + db.refresh(db_subtask) + return db_subtask + +@app.delete("/subtasks/{subtask_id}", response_model=SubtaskResponse) +def delete_subtask(subtask_id: int, db: Session = Depends(get_db)): + """Delete a specific subtask""" + db_subtask = db.query(Subtask).filter(Subtask.id == subtask_id).first() + if db_subtask is None: + raise HTTPException(status_code=404, detail="Subtask not found") + + db.delete(db_subtask) + db.commit() + return db_subtask + +@app.put("/todos/{todo_id}/complete-all-subtasks", response_model=TodoResponse) +def complete_all_subtasks(todo_id: int, db: Session = Depends(get_db)): + """Mark all subtasks of a todo as completed""" + 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.query(Subtask).filter(Subtask.todo_id == todo_id).update({"completed": True}) + db.commit() + db.refresh(db_todo) + return db_todo + if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/migrations/versions/004_add_subtasks_and_reminders.py b/migrations/versions/004_add_subtasks_and_reminders.py new file mode 100644 index 0000000..80a652d --- /dev/null +++ b/migrations/versions/004_add_subtasks_and_reminders.py @@ -0,0 +1,46 @@ +"""Add subtasks and reminders + +Revision ID: 004 +Revises: 003_add_tags_table_and_associations +Create Date: 2025-05-12 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '004' +down_revision = '003_add_tags_table_and_associations' +branch_labels = None +depends_on = None + + +def upgrade(): + # Add remind_at column to todos table + op.add_column('todos', sa.Column('remind_at', sa.DateTime(timezone=True), nullable=True)) + + # Create subtasks table + op.create_table( + 'subtasks', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('completed', sa.Boolean(), default=False), + sa.Column('todo_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(timezone=True), onupdate=sa.func.now()), + sa.ForeignKeyConstraint(['todo_id'], ['todos.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + + # Add index on todo_id for faster lookups + op.create_index(op.f('ix_subtasks_todo_id'), 'subtasks', ['todo_id'], unique=False) + + +def downgrade(): + # Drop subtasks table + op.drop_index(op.f('ix_subtasks_todo_id'), table_name='subtasks') + op.drop_table('subtasks') + + # Remove remind_at column from todos table + op.drop_column('todos', 'remind_at') \ No newline at end of file