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
|
- 📝 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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -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"])
|
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,
|
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
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 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
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 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")
|
@ -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
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 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
|
||||||
|
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