Enhance todo app with advanced features

- Add User model with email-based authentication
- Add Tag model with many-to-many relationship to todos
- Add TodoTag junction table for todo-tag relationships
- Enhance Todo model with priority levels (low, medium, high)
- Add due_date field with datetime support
- Add recurrence_pattern field for recurring todos
- Add parent-child relationship for subtasks support
- Create comprehensive alembic migration for all changes
- Add proper indexes for performance optimization
- Use Text type for todo descriptions
- Implement proper SQLAlchemy relationships and foreign keys
This commit is contained in:
Automated Action 2025-06-19 12:49:26 +00:00
parent 7a43bab909
commit 4b391fe54b
5 changed files with 439 additions and 21 deletions

View File

@ -0,0 +1,110 @@
"""Enhance todo features with categories, priorities, users, and subtasks
Revision ID: 002
Revises: 001
Create Date: 2024-01-02 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:
# Create users table
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
# Create tags table
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=False),
sa.Column('color', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tags_id'), 'tags', ['id'], unique=False)
op.create_index(op.f('ix_tags_name'), 'tags', ['name'], unique=True)
# Create todo_tags junction table
op.create_table('todo_tags',
sa.Column('todo_id', sa.Integer(), nullable=False),
sa.Column('tag_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.ForeignKeyConstraint(['todo_id'], ['todos.id'], ),
sa.PrimaryKeyConstraint('todo_id', 'tag_id')
)
# Add new columns to todos table
op.add_column('todos', sa.Column('priority', sa.Enum('LOW', 'MEDIUM', 'HIGH', name='prioritylevel'), server_default='MEDIUM', nullable=False))
op.add_column('todos', sa.Column('due_date', sa.DateTime(timezone=True), nullable=True))
op.add_column('todos', sa.Column('recurrence_pattern', sa.Enum('NONE', 'DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY', name='recurrencepattern'), server_default='NONE', nullable=False))
op.add_column('todos', sa.Column('user_id', sa.Integer(), nullable=False))
op.add_column('todos', sa.Column('parent_id', sa.Integer(), nullable=True))
# Change description column type to Text
op.alter_column('todos', 'description',
existing_type=sa.String(),
type_=sa.Text(),
existing_nullable=True)
# Create foreign key constraints
op.create_foreign_key('fk_todos_user_id', 'todos', 'users', ['user_id'], ['id'])
op.create_foreign_key('fk_todos_parent_id', 'todos', 'todos', ['parent_id'], ['id'])
# Create indexes for new columns
op.create_index(op.f('ix_todos_priority'), 'todos', ['priority'], unique=False)
op.create_index(op.f('ix_todos_due_date'), 'todos', ['due_date'], unique=False)
op.create_index(op.f('ix_todos_user_id'), 'todos', ['user_id'], unique=False)
op.create_index(op.f('ix_todos_parent_id'), 'todos', ['parent_id'], unique=False)
def downgrade() -> None:
# Drop indexes
op.drop_index(op.f('ix_todos_parent_id'), table_name='todos')
op.drop_index(op.f('ix_todos_user_id'), table_name='todos')
op.drop_index(op.f('ix_todos_due_date'), table_name='todos')
op.drop_index(op.f('ix_todos_priority'), table_name='todos')
# Drop foreign key constraints
op.drop_constraint('fk_todos_parent_id', 'todos', type_='foreignkey')
op.drop_constraint('fk_todos_user_id', 'todos', type_='foreignkey')
# Revert description column type back to String
op.alter_column('todos', 'description',
existing_type=sa.Text(),
type_=sa.String(),
existing_nullable=True)
# Drop new columns from todos table
op.drop_column('todos', 'parent_id')
op.drop_column('todos', 'user_id')
op.drop_column('todos', 'recurrence_pattern')
op.drop_column('todos', 'due_date')
op.drop_column('todos', 'priority')
# Drop todo_tags table
op.drop_table('todo_tags')
# Drop tags table
op.drop_index(op.f('ix_tags_name'), table_name='tags')
op.drop_index(op.f('ix_tags_id'), table_name='tags')
op.drop_table('tags')
# Drop users table
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_table('users')

View File

@ -1,13 +1,75 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from enum import Enum as PyEnum
from .db.base import Base
class PriorityLevel(PyEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class RecurrencePattern(PyEnum):
NONE = "none"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
YEARLY = "yearly"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
name = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Relationship with todos
todos = relationship("Todo", back_populates="user")
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True, nullable=False)
color = Column(String, nullable=True) # Optional color for UI
created_at = Column(DateTime(timezone=True), server_default=func.now())
class TodoTag(Base):
__tablename__ = "todo_tags"
todo_id = Column(Integer, ForeignKey("todos.id"), primary_key=True)
tag_id = Column(Integer, ForeignKey("tags.id"), primary_key=True)
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True, nullable=False)
description = Column(String, nullable=True)
description = Column(Text, nullable=True)
completed = Column(Boolean, default=False, nullable=False)
priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM, nullable=False, index=True)
due_date = Column(DateTime(timezone=True), nullable=True, index=True)
recurrence_pattern = Column(Enum(RecurrencePattern), default=RecurrencePattern.NONE, nullable=False)
# User relationship
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
user = relationship("User", back_populates="todos")
# Parent-child relationship for subtasks
parent_id = Column(Integer, ForeignKey("todos.id"), nullable=True, index=True)
parent = relationship("Todo", remote_side=[id], back_populates="subtasks")
subtasks = relationship("Todo", back_populates="parent")
# Many-to-many relationship with tags
tags = relationship("Tag", secondary="todo_tags", backref="todos")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

View File

@ -1,24 +1,257 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field, EmailStr, validator
from datetime import datetime
from typing import Optional
from typing import Optional, List
from enum import Enum
class TodoBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
class TodoCreate(TodoBase):
pass
# Enums for validation
class PriorityLevel(str, Enum):
"""Priority levels for todos"""
class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class RecurrencePattern(str, Enum):
"""Recurrence patterns for todos"""
NONE = "none"
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
YEARLY = "yearly"
# User schemas
class UserBase(BaseModel):
"""Base user schema with common fields"""
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
full_name: Optional[str] = Field(None, max_length=100)
class UserCreate(UserBase):
"""Schema for creating a new user"""
password: str = Field(..., min_length=6)
class UserUpdate(BaseModel):
"""Schema for updating user information"""
username: Optional[str] = Field(None, min_length=3, max_length=50)
email: Optional[EmailStr] = None
full_name: Optional[str] = Field(None, max_length=100)
password: Optional[str] = Field(None, min_length=6)
class User(UserBase):
"""User response schema"""
class Todo(TodoBase):
id: int
is_active: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
from_attributes = True
class UserInDB(User):
"""User schema with password hash for internal use"""
hashed_password: str
# Enhanced Todo schemas
class TodoBase(BaseModel):
"""Base todo schema with common fields"""
title: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
completed: bool = False
priority: PriorityLevel = PriorityLevel.MEDIUM
tags: List[str] = Field(default_factory=list, max_items=10)
due_date: Optional[datetime] = None
parent_id: Optional[int] = Field(None, description="ID of parent todo for subtasks")
recurrence_pattern: RecurrencePattern = RecurrencePattern.NONE
user_id: Optional[int] = Field(
None, description="ID of the user who owns this todo"
)
@validator("tags")
def validate_tags(cls, v):
"""Validate tags are non-empty strings and remove duplicates"""
if v:
# Remove empty strings and duplicates
clean_tags = list(set([tag.strip() for tag in v if tag.strip()]))
# Validate tag length
for tag in clean_tags:
if len(tag) > 50:
raise ValueError("Each tag must be 50 characters or less")
return clean_tags
return v
@validator("due_date")
def validate_due_date(cls, v):
"""Validate due date is not in the past"""
if v and v < datetime.now():
raise ValueError("Due date cannot be in the past")
return v
class TodoCreate(TodoBase):
"""Schema for creating a new todo"""
pass
class TodoUpdate(BaseModel):
"""Schema for updating todo information"""
title: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
completed: Optional[bool] = None
priority: Optional[PriorityLevel] = None
tags: Optional[List[str]] = Field(None, max_items=10)
due_date: Optional[datetime] = None
parent_id: Optional[int] = Field(None, description="ID of parent todo for subtasks")
recurrence_pattern: Optional[RecurrencePattern] = None
@validator("tags")
def validate_tags(cls, v):
"""Validate tags are non-empty strings and remove duplicates"""
if v is not None:
# Remove empty strings and duplicates
clean_tags = list(set([tag.strip() for tag in v if tag.strip()]))
# Validate tag length
for tag in clean_tags:
if len(tag) > 50:
raise ValueError("Each tag must be 50 characters or less")
return clean_tags
return v
@validator("due_date")
def validate_due_date(cls, v):
"""Validate due date is not in the past"""
if v and v < datetime.now():
raise ValueError("Due date cannot be in the past")
return v
class Todo(TodoBase):
"""Todo response schema with all fields"""
id: int
created_at: datetime
updated_at: datetime
subtasks: List["Todo"] = Field(default_factory=list, description="List of subtasks")
shared_with: List[User] = Field(
default_factory=list, description="Users this todo is shared with"
)
class Config:
from_attributes = True
class TodoSummary(BaseModel):
"""Simplified todo schema for list views"""
id: int
title: str
completed: bool
priority: PriorityLevel
tags: List[str]
due_date: Optional[datetime]
created_at: datetime
subtask_count: int = 0
class Config:
from_attributes = True
# Sharing schemas
class TodoShareBase(BaseModel):
"""Base schema for todo sharing"""
todo_id: int
user_id: int
can_edit: bool = False
class TodoShareCreate(TodoShareBase):
"""Schema for creating a todo share"""
pass
class TodoShare(TodoShareBase):
"""Todo share response schema"""
id: int
shared_at: datetime
todo: TodoSummary
user: User
class Config:
from_attributes = True
# Response schemas for collections
class TodoListResponse(BaseModel):
"""Response schema for todo lists with pagination"""
todos: List[TodoSummary]
total: int
page: int
page_size: int
has_next: bool
has_previous: bool
class UserListResponse(BaseModel):
"""Response schema for user lists with pagination"""
users: List[User]
total: int
page: int
page_size: int
has_next: bool
has_previous: bool
# Authentication schemas
class Token(BaseModel):
"""JWT token response"""
access_token: str
token_type: str
class TokenData(BaseModel):
"""Token data for JWT validation"""
username: Optional[str] = None
# API Response schemas
class APIResponse(BaseModel):
"""Generic API response wrapper"""
success: bool
message: str
data: Optional[dict] = None
class HealthResponse(BaseModel):
"""Health check response schema"""
status: str
timestamp: datetime
version: str = "1.0.0"
# Update Todo model to handle forward references
Todo.model_rebuild()

20
main.py
View File

@ -2,18 +2,27 @@ from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import List
from enum import Enum
from app.db.base import engine, Base
from app.db.session import get_db
from app import crud, schemas
# Priority levels enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
URGENT = "urgent"
# Create database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="Todo App API",
description="A simple todo application API",
version="1.0.0"
title="Enhanced Todo App API",
description="A comprehensive todo application API with categories, priorities, due dates, subtasks, users, and recurring todos",
version="2.0.0",
openapi_url="/openapi.json"
)
# CORS configuration
@ -28,8 +37,11 @@ app.add_middleware(
@app.get("/")
async def root():
return {
"title": "Todo App API",
"title": "Enhanced Todo App API",
"version": "2.0.0",
"documentation": "/docs",
"redoc": "/redoc",
"openapi": "/openapi.json",
"health": "/health"
}

View File

@ -2,4 +2,5 @@ fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.12.1
ruff==0.1.6
ruff==0.1.6
email-validator==2.1.0