Add categories and tags features

- Create Category and Tag models
- Create TodoTag association table
- Add category_id to Todo model
- Create Alembic migration for new tables
- Create schemas for Category and Tag
- Update Todo schemas to include Category and Tags
- Create CRUD operations for Categories and Tags
- Update Todo CRUD operations to handle categories and tags
- Create API endpoints for categories and tags
- Update Todo API endpoints with category and tag filtering
- Update documentation
This commit is contained in:
Automated Action 2025-06-17 02:52:26 +00:00
parent 5b2cda28a1
commit d60767d0ba
16 changed files with 626 additions and 8 deletions

View File

@ -11,6 +11,8 @@ A powerful, secure RESTful API for managing tasks and todos, built with FastAPI
- 📝 Todo CRUD operations - 📝 Todo CRUD operations
- Priority levels (High, Medium, Low) - Priority levels (High, Medium, Low)
- Due dates for better task management - Due dates for better task management
- Categories for task organization
- Tags for flexible grouping and filtering
- Smart ordering by priority and due date - Smart ordering by priority and due date
- 👤 User management - 👤 User management
- 🔍 Advanced todo filtering and pagination - 🔍 Advanced todo filtering and pagination
@ -93,6 +95,22 @@ The application can be configured using the following environment variables:
- `PUT /api/v1/todos/{id}` - Update a todo - `PUT /api/v1/todos/{id}` - Update a todo
- `DELETE /api/v1/todos/{id}` - Delete a todo - `DELETE /api/v1/todos/{id}` - Delete a todo
### Categories
- `GET /api/v1/categories/` - List all categories
- `POST /api/v1/categories/` - Create a new category
- `GET /api/v1/categories/{id}` - Get category by ID
- `PUT /api/v1/categories/{id}` - Update a category
- `DELETE /api/v1/categories/{id}` - Delete a category
### Tags
- `GET /api/v1/tags/` - List all tags
- `POST /api/v1/tags/` - Create a new tag
- `GET /api/v1/tags/{id}` - Get tag by ID
- `PUT /api/v1/tags/{id}` - Update a tag
- `DELETE /api/v1/tags/{id}` - Delete a tag
#### Todo Filtering #### Todo Filtering
The `GET /api/v1/todos/` endpoint supports the following query parameters: The `GET /api/v1/todos/` endpoint supports the following query parameters:
@ -104,6 +122,8 @@ The `GET /api/v1/todos/` endpoint supports the following query parameters:
- `priority`: Filter by priority level (low, medium, high) - `priority`: Filter by priority level (low, medium, high)
- `due_date_before`: Filter for todos due before this date - `due_date_before`: Filter for todos due before this date
- `due_date_after`: Filter for todos due after this date - `due_date_after`: Filter for todos due after this date
- `category_id`: Filter by category ID
- `tag_id`: Filter by tag ID
## Database Schema ## Database Schema
@ -126,9 +146,34 @@ description: Text (Optional)
is_completed: Boolean (Default: False) is_completed: Boolean (Default: False)
priority: Enum(low, medium, high) (Default: medium) priority: Enum(low, medium, high) (Default: medium)
due_date: DateTime (Optional) due_date: DateTime (Optional)
category_id: Integer (Foreign Key to Category, Optional)
owner_id: Integer (Foreign Key to User) owner_id: Integer (Foreign Key to User)
``` ```
### Category Model
```
id: Integer (Primary Key)
name: String (Unique, Indexed)
description: String (Optional)
owner_id: Integer (Foreign Key to User)
```
### Tag Model
```
id: Integer (Primary Key)
name: String (Unique, Indexed)
owner_id: Integer (Foreign Key to User)
```
### TodoTag Association Table
```
todo_id: Integer (Foreign Key to Todo, Primary Key)
tag_id: Integer (Foreign Key to Tag, Primary Key)
```
### RefreshToken Model ### RefreshToken Model
``` ```

View File

@ -1,9 +1,11 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.v1.endpoints import admin, auth, todos, users from app.api.v1.endpoints import admin, auth, categories, tags, todos, users
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"]) api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(todos.router, prefix="/todos", tags=["todos"]) api_router.include_router(todos.router, prefix="/todos", tags=["todos"])
api_router.include_router(categories.router, prefix="/categories", tags=["categories"])
api_router.include_router(tags.router, prefix="/tags", tags=["tags"])
api_router.include_router(admin.router, prefix="/admin", tags=["admin"]) api_router.include_router(admin.router, prefix="/admin", tags=["admin"])

View File

@ -0,0 +1,111 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_user, get_db
from app.crud.crud_category import (
create_category,
delete_category,
get_categories,
get_category,
get_category_by_name,
update_category,
)
from app.models.user import User
from app.schemas.category import Category, CategoryCreate, CategoryUpdate
router = APIRouter()
@router.get("/", response_model=List[Category])
def read_categories(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve categories.
"""
categories = get_categories(db=db, owner_id=current_user.id, skip=skip, limit=limit)
return categories
@router.post("/", response_model=Category)
def create_category_item(
*,
db: Session = Depends(get_db),
category_in: CategoryCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new category.
"""
category = get_category_by_name(
db=db, name=category_in.name, owner_id=current_user.id
)
if category:
raise HTTPException(
status_code=400,
detail="The category with this name already exists",
)
category = create_category(db=db, category_in=category_in, owner_id=current_user.id)
return category
@router.get("/{id}", response_model=Category)
def read_category(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get category by ID.
"""
category = get_category(db=db, category_id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
if category.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
return category
@router.put("/{id}", response_model=Category)
def update_category_item(
*,
db: Session = Depends(get_db),
id: int,
category_in: CategoryUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a category.
"""
category = get_category(db=db, category_id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
if category.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
category = update_category(db=db, db_obj=category, obj_in=category_in)
return category
@router.delete("/{id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
def delete_category_item(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a category.
"""
category = get_category(db=db, category_id=id)
if not category:
raise HTTPException(status_code=404, detail="Category not found")
if category.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
delete_category(db=db, category_id=id)
return None

View File

@ -0,0 +1,109 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_user, get_db
from app.crud.crud_tag import (
create_tag,
delete_tag,
get_tag,
get_tag_by_name,
get_tags,
update_tag,
)
from app.models.user import User
from app.schemas.tag import Tag, TagCreate, TagUpdate
router = APIRouter()
@router.get("/", response_model=List[Tag])
def read_tags(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve tags.
"""
tags = get_tags(db=db, owner_id=current_user.id, skip=skip, limit=limit)
return tags
@router.post("/", response_model=Tag)
def create_tag_item(
*,
db: Session = Depends(get_db),
tag_in: TagCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new tag.
"""
tag = get_tag_by_name(db=db, name=tag_in.name, owner_id=current_user.id)
if tag:
raise HTTPException(
status_code=400,
detail="The tag with this name already exists",
)
tag = create_tag(db=db, tag_in=tag_in, owner_id=current_user.id)
return tag
@router.get("/{id}", response_model=Tag)
def read_tag(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get tag by ID.
"""
tag = get_tag(db=db, tag_id=id)
if not tag:
raise HTTPException(status_code=404, detail="Tag not found")
if tag.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
return tag
@router.put("/{id}", response_model=Tag)
def update_tag_item(
*,
db: Session = Depends(get_db),
id: int,
tag_in: TagUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a tag.
"""
tag = get_tag(db=db, tag_id=id)
if not tag:
raise HTTPException(status_code=404, detail="Tag not found")
if tag.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
tag = update_tag(db=db, db_obj=tag, obj_in=tag_in)
return tag
@router.delete("/{id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
def delete_tag_item(
*,
db: Session = Depends(get_db),
id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a tag.
"""
tag = get_tag(db=db, tag_id=id)
if not tag:
raise HTTPException(status_code=404, detail="Tag not found")
if tag.owner_id != current_user.id:
raise HTTPException(status_code=400, detail="Not enough permissions")
delete_tag(db=db, tag_id=id)
return None

View File

@ -29,6 +29,8 @@ def read_todos(
priority: Optional[PriorityLevel] = None, priority: Optional[PriorityLevel] = None,
due_date_before: Optional[datetime] = None, due_date_before: Optional[datetime] = None,
due_date_after: Optional[datetime] = None, due_date_after: Optional[datetime] = None,
category_id: Optional[int] = None,
tag_id: Optional[int] = None,
current_user: User = Depends(get_current_active_user), current_user: User = Depends(get_current_active_user),
) -> Any: ) -> Any:
""" """
@ -41,6 +43,8 @@ def read_todos(
- **priority**: Filter by priority level (low, medium, high) - **priority**: Filter by priority level (low, medium, high)
- **due_date_before**: Filter for todos due before this date - **due_date_before**: Filter for todos due before this date
- **due_date_after**: Filter for todos due after this date - **due_date_after**: Filter for todos due after this date
- **category_id**: Filter by category ID
- **tag_id**: Filter by tag ID
""" """
todos = get_todos( todos = get_todos(
db=db, db=db,
@ -51,7 +55,9 @@ def read_todos(
is_completed=is_completed, is_completed=is_completed,
priority=priority, priority=priority,
due_date_before=due_date_before, due_date_before=due_date_before,
due_date_after=due_date_after due_date_after=due_date_after,
category_id=category_id,
tag_id=tag_id
) )
return todos return todos

51
app/crud/crud_category.py Normal file
View File

@ -0,0 +1,51 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.category import Category
from app.schemas.category import CategoryCreate, CategoryUpdate
def get_category(db: Session, category_id: int) -> Optional[Category]:
return db.query(Category).filter(Category.id == category_id).first()
def get_category_by_name(db: Session, name: str, owner_id: int) -> Optional[Category]:
return db.query(Category).filter(Category.name == name, Category.owner_id == owner_id).first()
def get_categories(
db: Session, owner_id: int, skip: int = 0, limit: int = 100
) -> List[Category]:
return db.query(Category).filter(Category.owner_id == owner_id).offset(skip).limit(limit).all()
def create_category(db: Session, category_in: CategoryCreate, owner_id: int) -> Category:
db_category = Category(
name=category_in.name,
description=category_in.description,
owner_id=owner_id
)
db.add(db_category)
db.commit()
db.refresh(db_category)
return db_category
def update_category(
db: Session, db_obj: Category, obj_in: CategoryUpdate
) -> Category:
update_data = obj_in.model_dump(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete_category(db: Session, category_id: int) -> Category:
category = db.query(Category).filter(Category.id == category_id).first()
db.delete(category)
db.commit()
return category

54
app/crud/crud_tag.py Normal file
View File

@ -0,0 +1,54 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.tag import Tag
from app.schemas.tag import TagCreate, TagUpdate
def get_tag(db: Session, tag_id: int) -> Optional[Tag]:
return db.query(Tag).filter(Tag.id == tag_id).first()
def get_tag_by_name(db: Session, name: str, owner_id: int) -> Optional[Tag]:
return db.query(Tag).filter(Tag.name == name, Tag.owner_id == owner_id).first()
def get_tags(
db: Session, owner_id: int, skip: int = 0, limit: int = 100
) -> List[Tag]:
return db.query(Tag).filter(Tag.owner_id == owner_id).offset(skip).limit(limit).all()
def get_tags_by_ids(db: Session, tag_ids: List[int], owner_id: int) -> List[Tag]:
return db.query(Tag).filter(Tag.id.in_(tag_ids), Tag.owner_id == owner_id).all()
def create_tag(db: Session, tag_in: TagCreate, owner_id: int) -> Tag:
db_tag = Tag(
name=tag_in.name,
owner_id=owner_id
)
db.add(db_tag)
db.commit()
db.refresh(db_tag)
return db_tag
def update_tag(
db: Session, db_obj: Tag, obj_in: TagUpdate
) -> Tag:
update_data = obj_in.model_dump(exclude_unset=True)
for field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete_tag(db: Session, tag_id: int) -> Tag:
tag = db.query(Tag).filter(Tag.id == tag_id).first()
db.delete(tag)
db.commit()
return tag

View File

@ -3,6 +3,7 @@ from typing import List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.crud.crud_tag import get_tags_by_ids
from app.models.todo import PriorityLevel, Todo from app.models.todo import PriorityLevel, Todo
from app.schemas.todo import TodoCreate, TodoUpdate from app.schemas.todo import TodoCreate, TodoUpdate
@ -20,7 +21,9 @@ def get_todos(
is_completed: Optional[bool] = None, is_completed: Optional[bool] = None,
priority: Optional[PriorityLevel] = None, priority: Optional[PriorityLevel] = None,
due_date_before: Optional[datetime] = None, due_date_before: Optional[datetime] = None,
due_date_after: Optional[datetime] = None due_date_after: Optional[datetime] = None,
category_id: Optional[int] = None,
tag_id: Optional[int] = None
) -> List[Todo]: ) -> List[Todo]:
query = db.query(Todo).filter(Todo.owner_id == owner_id) query = db.query(Todo).filter(Todo.owner_id == owner_id)
@ -39,6 +42,12 @@ def get_todos(
if due_date_after is not None: if due_date_after is not None:
query = query.filter(Todo.due_date >= due_date_after) query = query.filter(Todo.due_date >= due_date_after)
if category_id is not None:
query = query.filter(Todo.category_id == category_id)
if tag_id is not None:
query = query.join(Todo.tags).filter(Todo.tags.any(id=tag_id))
# Order by priority (highest first) and due date (earliest first) # Order by priority (highest first) and due date (earliest first)
query = query.order_by(Todo.priority.desc(), Todo.due_date.asc()) query = query.order_by(Todo.priority.desc(), Todo.due_date.asc())
@ -52,11 +61,21 @@ def create_todo(db: Session, todo_in: TodoCreate, owner_id: int) -> Todo:
is_completed=todo_in.is_completed, is_completed=todo_in.is_completed,
priority=todo_in.priority, priority=todo_in.priority,
due_date=todo_in.due_date, due_date=todo_in.due_date,
category_id=todo_in.category_id,
owner_id=owner_id owner_id=owner_id
) )
db.add(db_todo) db.add(db_todo)
db.commit() db.commit()
db.refresh(db_todo) db.refresh(db_todo)
# Add tags if provided
if todo_in.tags:
tags = get_tags_by_ids(db, todo_in.tags, owner_id)
db_todo.tags = tags
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo return db_todo
@ -64,11 +83,26 @@ def update_todo(
db: Session, db_obj: Todo, obj_in: TodoUpdate db: Session, db_obj: Todo, obj_in: TodoUpdate
) -> Todo: ) -> Todo:
update_data = obj_in.model_dump(exclude_unset=True) update_data = obj_in.model_dump(exclude_unset=True)
# Handle tags separately
tags_data = update_data.pop("tags", None)
# Update other fields
for field in update_data: for field in update_data:
setattr(db_obj, field, update_data[field]) setattr(db_obj, field, update_data[field])
db.add(db_obj) db.add(db_obj)
db.commit() db.commit()
db.refresh(db_obj) db.refresh(db_obj)
# Update tags if provided
if tags_data is not None:
tags = get_tags_by_ids(db, tags_data, db_obj.owner_id)
db_obj.tags = tags
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj return db_obj

16
app/models/category.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from app.db.base import Base
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, unique=True)
description = Column(String, nullable=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="categories")
todos = relationship("Todo", back_populates="category")

24
app/models/tag.py Normal file
View File

@ -0,0 +1,24 @@
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import relationship
from app.db.base import Base
# Association table for many-to-many relationship between Todo and Tag
todo_tag = Table(
"todo_tag",
Base.metadata,
Column("todo_id", Integer, ForeignKey("todos.id"), primary_key=True),
Column("tag_id", Integer, ForeignKey("tags.id"), primary_key=True),
)
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, unique=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", back_populates="tags")
todos = relationship("Todo", secondary=todo_tag, back_populates="tags")

View File

@ -3,6 +3,7 @@ from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, Str
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from app.db.base import Base from app.db.base import Base
from app.models.tag import todo_tag
class PriorityLevel(str, PyEnum): class PriorityLevel(str, PyEnum):
@ -21,5 +22,8 @@ class Todo(Base):
priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM) priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM)
due_date = Column(DateTime, nullable=True) due_date = Column(DateTime, nullable=True)
owner_id = Column(Integer, ForeignKey("users.id")) owner_id = Column(Integer, ForeignKey("users.id"))
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
owner = relationship("User", back_populates="todos") owner = relationship("User", back_populates="todos")
category = relationship("Category", back_populates="todos")
tags = relationship("Tag", secondary=todo_tag, back_populates="todos")

View File

@ -20,3 +20,5 @@ class User(Base):
role = Column(Enum(UserRole), default=UserRole.USER) role = Column(Enum(UserRole), default=UserRole.USER)
todos = relationship("Todo", back_populates="owner") todos = relationship("Todo", back_populates="owner")
categories = relationship("Category", back_populates="owner")
tags = relationship("Tag", back_populates="owner")

39
app/schemas/category.py Normal file
View File

@ -0,0 +1,39 @@
from typing import Optional
from pydantic import BaseModel
# Shared properties
class CategoryBase(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
# Properties to receive on category creation
class CategoryCreate(CategoryBase):
name: str
# Properties to receive on category update
class CategoryUpdate(CategoryBase):
pass
# Properties shared by models stored in DB
class CategoryInDBBase(CategoryBase):
id: int
name: str
owner_id: int
class Config:
from_attributes = True
# Properties to return to client
class Category(CategoryInDBBase):
pass
# Properties properties stored in DB
class CategoryInDB(CategoryInDBBase):
pass

38
app/schemas/tag.py Normal file
View File

@ -0,0 +1,38 @@
from typing import Optional
from pydantic import BaseModel
# Shared properties
class TagBase(BaseModel):
name: Optional[str] = None
# Properties to receive on tag creation
class TagCreate(TagBase):
name: str
# Properties to receive on tag update
class TagUpdate(TagBase):
pass
# Properties shared by models stored in DB
class TagInDBBase(TagBase):
id: int
name: str
owner_id: int
class Config:
from_attributes = True
# Properties to return to client
class Tag(TagInDBBase):
pass
# Properties properties stored in DB
class TagInDB(TagInDBBase):
pass

View File

@ -1,9 +1,11 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from app.models.todo import PriorityLevel from app.models.todo import PriorityLevel
from app.schemas.category import Category
from app.schemas.tag import Tag
# Shared properties # Shared properties
@ -13,16 +15,18 @@ class TodoBase(BaseModel):
is_completed: Optional[bool] = False is_completed: Optional[bool] = False
priority: Optional[PriorityLevel] = PriorityLevel.MEDIUM priority: Optional[PriorityLevel] = PriorityLevel.MEDIUM
due_date: Optional[datetime] = None due_date: Optional[datetime] = None
category_id: Optional[int] = None
# Properties to receive on item creation # Properties to receive on item creation
class TodoCreate(TodoBase): class TodoCreate(TodoBase):
title: str title: str
tags: Optional[List[int]] = None # List of tag IDs
# Properties to receive on item update # Properties to receive on item update
class TodoUpdate(TodoBase): class TodoUpdate(TodoBase):
pass tags: Optional[List[int]] = None # List of tag IDs
# Properties shared by models stored in DB # Properties shared by models stored in DB
@ -37,7 +41,8 @@ class TodoInDBBase(TodoBase):
# Properties to return to client # Properties to return to client
class Todo(TodoInDBBase): class Todo(TodoInDBBase):
pass category: Optional[Category] = None
tags: List[Tag] = []
# Properties properties stored in DB # Properties properties stored in DB

View File

@ -0,0 +1,78 @@
"""add categories and tags
Revision ID: 005
Revises: 004
Create Date: 2023-11-18
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '005'
down_revision = '004'
branch_labels = None
depends_on = None
def upgrade():
# Create categories table
op.create_table(
'categories',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_categories_id'), 'categories', ['id'], unique=False)
op.create_index(op.f('ix_categories_name'), 'categories', ['name'], unique=True)
# Create tags table
op.create_table(
'tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
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_tag association table
op.create_table(
'todo_tag',
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 category_id to todos table
with op.batch_alter_table('todos', schema=None) as batch_op:
batch_op.add_column(sa.Column('category_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_todos_category_id', 'categories', ['category_id'], ['id'])
def downgrade():
# Remove category_id from todos table
with op.batch_alter_table('todos', schema=None) as batch_op:
batch_op.drop_constraint('fk_todos_category_id', type_='foreignkey')
batch_op.drop_column('category_id')
# Drop todo_tag association table
op.drop_table('todo_tag')
# 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 categories table
op.drop_index(op.f('ix_categories_name'), table_name='categories')
op.drop_index(op.f('ix_categories_id'), table_name='categories')
op.drop_table('categories')