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:
parent
5b2cda28a1
commit
d60767d0ba
45
README.md
45
README.md
@ -11,6 +11,8 @@ A powerful, secure RESTful API for managing tasks and todos, built with FastAPI
|
||||
- 📝 Todo CRUD operations
|
||||
- Priority levels (High, Medium, Low)
|
||||
- Due dates for better task management
|
||||
- Categories for task organization
|
||||
- Tags for flexible grouping and filtering
|
||||
- Smart ordering by priority and due date
|
||||
- 👤 User management
|
||||
- 🔍 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
|
||||
- `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
|
||||
|
||||
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)
|
||||
- `due_date_before`: Filter for todos due before 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
|
||||
|
||||
@ -126,9 +146,34 @@ description: Text (Optional)
|
||||
is_completed: Boolean (Default: False)
|
||||
priority: Enum(low, medium, high) (Default: medium)
|
||||
due_date: DateTime (Optional)
|
||||
category_id: Integer (Foreign Key to Category, Optional)
|
||||
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
|
||||
|
||||
```
|
||||
|
@ -1,9 +1,11 @@
|
||||
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.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
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"])
|
111
app/api/v1/endpoints/categories.py
Normal file
111
app/api/v1/endpoints/categories.py
Normal 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
|
109
app/api/v1/endpoints/tags.py
Normal file
109
app/api/v1/endpoints/tags.py
Normal 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
|
@ -29,6 +29,8 @@ def read_todos(
|
||||
priority: Optional[PriorityLevel] = None,
|
||||
due_date_before: 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),
|
||||
) -> Any:
|
||||
"""
|
||||
@ -41,6 +43,8 @@ def read_todos(
|
||||
- **priority**: Filter by priority level (low, medium, high)
|
||||
- **due_date_before**: Filter for todos due before 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(
|
||||
db=db,
|
||||
@ -51,7 +55,9 @@ def read_todos(
|
||||
is_completed=is_completed,
|
||||
priority=priority,
|
||||
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
|
||||
|
||||
|
51
app/crud/crud_category.py
Normal file
51
app/crud/crud_category.py
Normal 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
54
app/crud/crud_tag.py
Normal 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
|
@ -3,6 +3,7 @@ from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.crud_tag import get_tags_by_ids
|
||||
from app.models.todo import PriorityLevel, Todo
|
||||
from app.schemas.todo import TodoCreate, TodoUpdate
|
||||
|
||||
@ -20,7 +21,9 @@ def get_todos(
|
||||
is_completed: Optional[bool] = None,
|
||||
priority: Optional[PriorityLevel] = 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]:
|
||||
query = db.query(Todo).filter(Todo.owner_id == owner_id)
|
||||
|
||||
@ -39,6 +42,12 @@ def get_todos(
|
||||
if due_date_after is not None:
|
||||
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)
|
||||
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,
|
||||
priority=todo_in.priority,
|
||||
due_date=todo_in.due_date,
|
||||
category_id=todo_in.category_id,
|
||||
owner_id=owner_id
|
||||
)
|
||||
db.add(db_todo)
|
||||
db.commit()
|
||||
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
|
||||
|
||||
|
||||
@ -64,11 +83,26 @@ def update_todo(
|
||||
db: Session, db_obj: Todo, obj_in: TodoUpdate
|
||||
) -> Todo:
|
||||
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:
|
||||
setattr(db_obj, field, update_data[field])
|
||||
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
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
|
||||
|
||||
|
||||
|
16
app/models/category.py
Normal file
16
app/models/category.py
Normal 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
24
app/models/tag.py
Normal 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")
|
@ -3,6 +3,7 @@ from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, Str
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.base import Base
|
||||
from app.models.tag import todo_tag
|
||||
|
||||
|
||||
class PriorityLevel(str, PyEnum):
|
||||
@ -21,5 +22,8 @@ class Todo(Base):
|
||||
priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM)
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
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")
|
@ -19,4 +19,6 @@ class User(Base):
|
||||
is_active = Column(Boolean, default=True)
|
||||
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
39
app/schemas/category.py
Normal 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
38
app/schemas/tag.py
Normal 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
|
@ -1,9 +1,11 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.models.todo import PriorityLevel
|
||||
from app.schemas.category import Category
|
||||
from app.schemas.tag import Tag
|
||||
|
||||
|
||||
# Shared properties
|
||||
@ -13,16 +15,18 @@ class TodoBase(BaseModel):
|
||||
is_completed: Optional[bool] = False
|
||||
priority: Optional[PriorityLevel] = PriorityLevel.MEDIUM
|
||||
due_date: Optional[datetime] = None
|
||||
category_id: Optional[int] = None
|
||||
|
||||
|
||||
# Properties to receive on item creation
|
||||
class TodoCreate(TodoBase):
|
||||
title: str
|
||||
tags: Optional[List[int]] = None # List of tag IDs
|
||||
|
||||
|
||||
# Properties to receive on item update
|
||||
class TodoUpdate(TodoBase):
|
||||
pass
|
||||
tags: Optional[List[int]] = None # List of tag IDs
|
||||
|
||||
|
||||
# Properties shared by models stored in DB
|
||||
@ -37,7 +41,8 @@ class TodoInDBBase(TodoBase):
|
||||
|
||||
# Properties to return to client
|
||||
class Todo(TodoInDBBase):
|
||||
pass
|
||||
category: Optional[Category] = None
|
||||
tags: List[Tag] = []
|
||||
|
||||
|
||||
# Properties properties stored in DB
|
||||
|
78
migrations/versions/005_add_categories_and_tags.py
Normal file
78
migrations/versions/005_add_categories_and_tags.py
Normal 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')
|
Loading…
x
Reference in New Issue
Block a user