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:
parent
7a43bab909
commit
4b391fe54b
110
alembic/versions/002_enhance_todo_features.py
Normal file
110
alembic/versions/002_enhance_todo_features.py
Normal 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')
|
@ -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())
|
261
app/schemas.py
261
app/schemas.py
@ -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
20
main.py
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user