Add task completion tracking with timestamps
- Added completion_date field to Todo model - Modified CRUD to auto-set completion_date when task is marked complete - Added completion date stats (last 24h, last week metrics) - Created Alembic migration for the new field - Updated API documentation and README generated with BackendIM... (backend.im)
This commit is contained in:
parent
357c328042
commit
34f86c8479
14
README.md
14
README.md
@ -9,13 +9,14 @@ A simple Todo API application built with FastAPI and SQLite. This RESTful API pr
|
|||||||
- Todo categorization with categories
|
- Todo categorization with categories
|
||||||
- Priority levels (low, medium, high)
|
- Priority levels (low, medium, high)
|
||||||
- Due dates for task deadlines
|
- Due dates for task deadlines
|
||||||
|
- Task completion tracking with completion timestamps
|
||||||
- Advanced filtering capabilities:
|
- Advanced filtering capabilities:
|
||||||
- Filter by category
|
- Filter by category
|
||||||
- Filter by priority
|
- Filter by priority
|
||||||
- Filter by completion status
|
- Filter by completion status
|
||||||
- Filter by due date range
|
- Filter by due date range
|
||||||
- Customizable sorting options
|
- Customizable sorting options
|
||||||
- Todo statistics and analytics
|
- Todo statistics and analytics including completion metrics
|
||||||
- SQLAlchemy ORM with SQLite database
|
- SQLAlchemy ORM with SQLite database
|
||||||
- Alembic for database migrations
|
- Alembic for database migrations
|
||||||
- FastAPI's automatic OpenAPI documentation
|
- FastAPI's automatic OpenAPI documentation
|
||||||
@ -110,11 +111,14 @@ The GET /api/v1/todos/ endpoint supports the following query parameters:
|
|||||||
"category": "work",
|
"category": "work",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"due_date": "2025-05-20T12:00:00",
|
"due_date": "2025-05-20T12:00:00",
|
||||||
|
"completion_date": null,
|
||||||
"created_at": "2025-05-13T12:00:00",
|
"created_at": "2025-05-13T12:00:00",
|
||||||
"updated_at": null
|
"updated_at": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When a todo is marked as completed, the `completion_date` is automatically set to the current time. If a todo is marked as not completed, the `completion_date` is cleared.
|
||||||
|
|
||||||
## Statistics
|
## Statistics
|
||||||
|
|
||||||
The /api/v1/todos/stats endpoint returns the following information:
|
The /api/v1/todos/stats endpoint returns the following information:
|
||||||
@ -125,6 +129,8 @@ The /api/v1/todos/stats endpoint returns the following information:
|
|||||||
"completed": 3,
|
"completed": 3,
|
||||||
"incomplete": 7,
|
"incomplete": 7,
|
||||||
"overdue": 2,
|
"overdue": 2,
|
||||||
|
"completed_last_24h": 1,
|
||||||
|
"completed_last_week": 3,
|
||||||
"by_category": {
|
"by_category": {
|
||||||
"work": 4,
|
"work": 4,
|
||||||
"personal": 3,
|
"personal": 3,
|
||||||
@ -137,4 +143,8 @@ The /api/v1/todos/stats endpoint returns the following information:
|
|||||||
"low": 2
|
"low": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The statistics include:
|
||||||
|
- `completed_last_24h`: Number of tasks completed within the last 24 hours
|
||||||
|
- `completed_last_week`: Number of tasks completed within the last 7 days
|
29
alembic/versions/6f42ebcf5b2f_add_completion_date.py
Normal file
29
alembic/versions/6f42ebcf5b2f_add_completion_date.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""add completion date
|
||||||
|
|
||||||
|
Revision ID: 6f42ebcf5b2f
|
||||||
|
Revises: 5f42ebcf5b2e
|
||||||
|
Create Date: 2025-05-13
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6f42ebcf5b2f'
|
||||||
|
down_revision = '5f42ebcf5b2e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Add completion_date column
|
||||||
|
op.add_column('todos', sa.Column('completion_date', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
|
||||||
|
# Set completion_date for already completed todos to their updated_at value
|
||||||
|
op.execute("UPDATE todos SET completion_date = updated_at WHERE completed = TRUE")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Drop column
|
||||||
|
op.drop_column('todos', 'completion_date')
|
@ -56,7 +56,13 @@ def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.get("/stats", response_model=Dict[str, Any])
|
@router.get("/stats", response_model=Dict[str, Any])
|
||||||
def get_todo_stats(db: Session = Depends(get_db)):
|
def get_todo_stats(db: Session = Depends(get_db)):
|
||||||
"""Get statistics about todos"""
|
"""Get statistics about todos including:
|
||||||
|
- Total, completed, and incomplete counts
|
||||||
|
- Number of overdue tasks
|
||||||
|
- Number of tasks completed in the last 24 hours
|
||||||
|
- Number of tasks completed in the last 7 days
|
||||||
|
- Breakdown by category and priority
|
||||||
|
"""
|
||||||
return crud.get_todo_stats(db=db)
|
return crud.get_todo_stats(db=db)
|
||||||
|
|
||||||
|
|
||||||
@ -71,7 +77,11 @@ def read_todo(todo_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.put("/{todo_id}", response_model=Todo)
|
@router.put("/{todo_id}", response_model=Todo)
|
||||||
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
|
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
|
||||||
"""Update a todo with new values including category, priority and due date"""
|
"""Update a todo with new values including category, priority, due date and completion status.
|
||||||
|
|
||||||
|
When a todo is marked as completed, the completion_date will be automatically set to the current time
|
||||||
|
if not explicitly provided. If a todo is marked as not completed, the completion_date will be cleared.
|
||||||
|
"""
|
||||||
db_todo = crud.update_todo(db=db, todo_id=todo_id, todo=todo)
|
db_todo = crud.update_todo(db=db, todo_id=todo_id, todo=todo)
|
||||||
if db_todo is None:
|
if db_todo is None:
|
||||||
raise HTTPException(status_code=404, detail="Todo not found")
|
raise HTTPException(status_code=404, detail="Todo not found")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import asc, desc
|
from sqlalchemy import asc, desc
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from app.models.todo import Todo, PriorityEnum
|
from app.models.todo import Todo, PriorityEnum
|
||||||
from app.schemas.todo import TodoCreate, TodoUpdate
|
from app.schemas.todo import TodoCreate, TodoUpdate
|
||||||
@ -75,6 +75,16 @@ def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
update_data = todo.model_dump(exclude_unset=True)
|
update_data = todo.model_dump(exclude_unset=True)
|
||||||
|
|
||||||
|
# If completed status is being set to True and there's no completion_date provided,
|
||||||
|
# set completion_date to current datetime
|
||||||
|
if update_data.get('completed') is True and 'completion_date' not in update_data:
|
||||||
|
update_data['completion_date'] = datetime.now()
|
||||||
|
|
||||||
|
# If completed status is being set to False, clear the completion_date
|
||||||
|
if update_data.get('completed') is False and db_todo.completed:
|
||||||
|
update_data['completion_date'] = None
|
||||||
|
|
||||||
for field, value in update_data.items():
|
for field, value in update_data.items():
|
||||||
setattr(db_todo, field, value)
|
setattr(db_todo, field, value)
|
||||||
|
|
||||||
@ -122,11 +132,27 @@ def get_todo_stats(db: Session) -> Dict[str, Any]:
|
|||||||
Todo.completed == False
|
Todo.completed == False
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
|
# Get completed todos in last 24 hours
|
||||||
|
yesterday = now - datetime.timedelta(days=1)
|
||||||
|
completed_last_24h = db.query(Todo).filter(
|
||||||
|
Todo.completion_date > yesterday,
|
||||||
|
Todo.completed == True
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Get completed todos in last 7 days
|
||||||
|
last_week = now - datetime.timedelta(days=7)
|
||||||
|
completed_last_week = db.query(Todo).filter(
|
||||||
|
Todo.completion_date > last_week,
|
||||||
|
Todo.completed == True
|
||||||
|
).count()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
"completed": completed,
|
"completed": completed,
|
||||||
"incomplete": incomplete,
|
"incomplete": incomplete,
|
||||||
"overdue": overdue,
|
"overdue": overdue,
|
||||||
|
"completed_last_24h": completed_last_24h,
|
||||||
|
"completed_last_week": completed_last_week,
|
||||||
"by_category": {cat or "uncategorized": count for cat, count in categories},
|
"by_category": {cat or "uncategorized": count for cat, count in categories},
|
||||||
"by_priority": {str(pri.name).lower() if pri else "none": count for pri, count in priorities}
|
"by_priority": {str(pri.name).lower() if pri else "none": count for pri, count in priorities}
|
||||||
}
|
}
|
@ -19,5 +19,6 @@ class Todo(Base):
|
|||||||
category = Column(String, nullable=True, index=True)
|
category = Column(String, nullable=True, index=True)
|
||||||
priority = Column(Enum(PriorityEnum), default=PriorityEnum.MEDIUM, nullable=True)
|
priority = Column(Enum(PriorityEnum), default=PriorityEnum.MEDIUM, nullable=True)
|
||||||
due_date = Column(DateTime(timezone=True), nullable=True)
|
due_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
completion_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
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())
|
@ -26,9 +26,11 @@ class TodoUpdate(BaseModel):
|
|||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
priority: Optional[PriorityEnum] = None
|
priority: Optional[PriorityEnum] = None
|
||||||
due_date: Optional[datetime] = None
|
due_date: Optional[datetime] = None
|
||||||
|
completion_date: Optional[datetime] = None
|
||||||
|
|
||||||
class TodoInDB(TodoBase):
|
class TodoInDB(TodoBase):
|
||||||
id: int
|
id: int
|
||||||
|
completion_date: Optional[datetime] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user