Update README with Categories system documentation
- Document category management features - Update API endpoints to include categories - Revise project structure to reflect current implementation - Add usage examples for categories and filtering - Remove references to unimplemented features - Update models documentation for Todo and Category
This commit is contained in:
parent
6432d4a805
commit
cf0d0e45b0
76
README.md
76
README.md
@ -5,6 +5,9 @@ A simple todo application built with FastAPI and SQLite.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Create, read, update, and delete todos
|
- Create, read, update, and delete todos
|
||||||
|
- Category management system for organizing todos
|
||||||
|
- Priority levels for todos (low, medium, high)
|
||||||
|
- Search and filtering by category, priority, and completion status
|
||||||
- SQLite database with SQLAlchemy ORM
|
- SQLite database with SQLAlchemy ORM
|
||||||
- Database migrations with Alembic
|
- Database migrations with Alembic
|
||||||
- FastAPI with automatic OpenAPI documentation
|
- FastAPI with automatic OpenAPI documentation
|
||||||
@ -13,16 +16,26 @@ A simple todo application built with FastAPI and SQLite.
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
|
### General
|
||||||
- `GET /` - Root endpoint with basic information
|
- `GET /` - Root endpoint with basic information
|
||||||
- `GET /health` - Health check endpoint
|
- `GET /health` - Health check endpoint
|
||||||
- `GET /docs` - Interactive API documentation (Swagger UI)
|
- `GET /docs` - Interactive API documentation (Swagger UI)
|
||||||
- `GET /redoc` - Alternative API documentation
|
- `GET /redoc` - Alternative API documentation
|
||||||
- `GET /api/v1/todos` - Get all todos
|
|
||||||
|
### Todos
|
||||||
|
- `GET /api/v1/todos` - Get all todos (with filtering by category, priority, completion status, and search)
|
||||||
- `POST /api/v1/todos` - Create a new todo
|
- `POST /api/v1/todos` - Create a new todo
|
||||||
- `GET /api/v1/todos/{todo_id}` - Get a specific todo
|
- `GET /api/v1/todos/{todo_id}` - Get a specific todo
|
||||||
- `PUT /api/v1/todos/{todo_id}` - Update a specific todo
|
- `PUT /api/v1/todos/{todo_id}` - Update a specific todo
|
||||||
- `DELETE /api/v1/todos/{todo_id}` - Delete a specific todo
|
- `DELETE /api/v1/todos/{todo_id}` - Delete a specific todo
|
||||||
|
|
||||||
|
### Categories
|
||||||
|
- `GET /api/v1/categories` - Get all categories
|
||||||
|
- `POST /api/v1/categories` - Create a new category
|
||||||
|
- `GET /api/v1/categories/{category_id}` - Get a specific category
|
||||||
|
- `PUT /api/v1/categories/{category_id}` - Update a specific category
|
||||||
|
- `DELETE /api/v1/categories/{category_id}` - Delete a specific category
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install the dependencies:
|
1. Install the dependencies:
|
||||||
@ -49,24 +62,30 @@ The API will be available at `http://localhost:8000`
|
|||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ └── v1/
|
│ │ └── v1/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── todos.py
|
│ │ ├── todos.py
|
||||||
|
│ │ └── categories.py
|
||||||
│ ├── crud/
|
│ ├── crud/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── todo.py
|
│ │ ├── todo.py
|
||||||
|
│ │ └── category.py
|
||||||
│ ├── db/
|
│ ├── db/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── base.py
|
│ │ ├── base.py
|
||||||
│ │ └── session.py
|
│ │ └── session.py
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── todo.py
|
│ │ ├── todo.py
|
||||||
|
│ │ └── category.py
|
||||||
│ ├── schemas/
|
│ ├── schemas/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── todo.py
|
│ │ ├── todo.py
|
||||||
|
│ │ └── category.py
|
||||||
│ └── __init__.py
|
│ └── __init__.py
|
||||||
├── alembic/
|
├── alembic/
|
||||||
│ ├── versions/
|
│ ├── versions/
|
||||||
│ │ └── 001_initial_todo_table.py
|
│ │ ├── 001_initial_todo_table.py
|
||||||
|
│ │ ├── 002_add_priority_field.py
|
||||||
|
│ │ └── 003_add_categories.py
|
||||||
│ ├── env.py
|
│ ├── env.py
|
||||||
│ └── script.py.mako
|
│ └── script.py.mako
|
||||||
├── alembic.ini
|
├── alembic.ini
|
||||||
@ -79,12 +98,55 @@ The API will be available at `http://localhost:8000`
|
|||||||
|
|
||||||
The application uses SQLite database stored at `/app/storage/db/db.sqlite`. The database schema is managed using Alembic migrations.
|
The application uses SQLite database stored at `/app/storage/db/db.sqlite`. The database schema is managed using Alembic migrations.
|
||||||
|
|
||||||
## Todo Model
|
## Models
|
||||||
|
|
||||||
|
### Todo Model
|
||||||
Each todo has the following fields:
|
Each todo has the following fields:
|
||||||
- `id`: Unique identifier (auto-generated)
|
- `id`: Unique identifier (auto-generated)
|
||||||
- `title`: Todo title (required, max 200 characters)
|
- `title`: Todo title (required, max 200 characters)
|
||||||
- `description`: Todo description (optional, max 500 characters)
|
- `description`: Todo description (optional, max 500 characters)
|
||||||
- `completed`: Completion status (boolean, default: false)
|
- `completed`: Completion status (boolean, default: false)
|
||||||
|
- `priority`: Priority level (low, medium, high, default: medium)
|
||||||
|
- `category_id`: Reference to category (optional)
|
||||||
- `created_at`: Creation timestamp
|
- `created_at`: Creation timestamp
|
||||||
- `updated_at`: Last update timestamp
|
- `updated_at`: Last update timestamp
|
||||||
|
|
||||||
|
### Category Model
|
||||||
|
Each category has the following fields:
|
||||||
|
- `id`: Unique identifier (auto-generated)
|
||||||
|
- `name`: Category name (required, max 100 characters, unique)
|
||||||
|
- `description`: Category description (optional, max 500 characters)
|
||||||
|
- `color`: Color code in hex format (optional, e.g., #FF0000)
|
||||||
|
- `created_at`: Creation timestamp
|
||||||
|
- `updated_at`: Last update timestamp
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Creating a Category
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/categories" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "Work", "description": "Work related tasks", "color": "#FF6B6B"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating a Todo with a Category
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v1/todos" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"title": "Review presentation", "description": "Review the quarterly presentation", "category_id": 1, "priority": "high"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering Todos by Category
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/api/v1/todos?category_id=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filtering Todos by Priority and Completion Status
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/api/v1/todos?priority=high&completed=false"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Todos
|
||||||
|
```bash
|
||||||
|
curl "http://localhost:8000/api/v1/todos?search=presentation"
|
||||||
|
```
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
"""Add subtasks support with parent_id field
|
|
||||||
|
|
||||||
Revision ID: 003
|
|
||||||
Revises: 002
|
|
||||||
Create Date: 2024-01-01 00:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "003"
|
|
||||||
down_revision = "002"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.add_column(
|
|
||||||
"todos",
|
|
||||||
sa.Column("parent_id", sa.Integer(), nullable=True),
|
|
||||||
)
|
|
||||||
# Add foreign key constraint
|
|
||||||
op.create_foreign_key(
|
|
||||||
"fk_todos_parent_id",
|
|
||||||
"todos",
|
|
||||||
"todos",
|
|
||||||
["parent_id"],
|
|
||||||
["id"],
|
|
||||||
ondelete="CASCADE",
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
# Drop foreign key constraint first
|
|
||||||
op.drop_constraint("fk_todos_parent_id", "todos", type_="foreignkey")
|
|
||||||
# Drop the column
|
|
||||||
op.drop_column("todos", "parent_id")
|
|
||||||
# ### end Alembic commands ###
|
|
@ -1,7 +1,7 @@
|
|||||||
"""Add projects table and project_id to todos
|
"""Add projects table and project_id to todos
|
||||||
|
|
||||||
Revision ID: 005_add_projects
|
Revision ID: 004_add_projects
|
||||||
Revises: 004_add_tags
|
Revises: 003_add_categories
|
||||||
Create Date: 2025-06-18 15:00:00.000000
|
Create Date: 2025-06-18 15:00:00.000000
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -13,8 +13,8 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = "005_add_projects"
|
revision: str = "004_add_projects"
|
||||||
down_revision: Union[str, None] = "004_add_tags"
|
down_revision: Union[str, None] = "003_add_categories"
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
@ -1,57 +0,0 @@
|
|||||||
"""Add tags and todo_tags tables
|
|
||||||
|
|
||||||
Revision ID: 004_add_tags
|
|
||||||
Revises: 003_add_project_category
|
|
||||||
Create Date: 2025-06-18 12:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = "004_add_tags"
|
|
||||||
down_revision: Union[str, None] = "003"
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# Create tags table
|
|
||||||
op.create_table(
|
|
||||||
"tags",
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("name", sa.String(length=50), nullable=False),
|
|
||||||
sa.Column("color", sa.String(length=7), nullable=False),
|
|
||||||
sa.Column(
|
|
||||||
"created_at",
|
|
||||||
sa.DateTime(timezone=True),
|
|
||||||
server_default=sa.text("now()"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("name"),
|
|
||||||
)
|
|
||||||
op.create_index(op.f("ix_tags_id"), "tags", ["id"], unique=False)
|
|
||||||
|
|
||||||
# Create todo_tags association 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"], ondelete="CASCADE"),
|
|
||||||
sa.ForeignKeyConstraint(["todo_id"], ["todos.id"], ondelete="CASCADE"),
|
|
||||||
sa.PrimaryKeyConstraint("todo_id", "tag_id"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# Drop todo_tags association table
|
|
||||||
op.drop_table("todo_tags")
|
|
||||||
|
|
||||||
# Drop tags table
|
|
||||||
op.drop_index(op.f("ix_tags_id"), table_name="tags")
|
|
||||||
op.drop_table("tags")
|
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
from typing import List
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.db.session import get_db
|
|
||||||
from app.crud import tag as tag_crud
|
|
||||||
from app.schemas.tag import Tag, TagCreate, TagUpdate, TagListResponse
|
|
||||||
from app.schemas.todo import Todo
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=TagListResponse)
|
|
||||||
def read_tags(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Retrieve tags with pagination.
|
|
||||||
"""
|
|
||||||
tags = tag_crud.get_tags(db, skip=skip, limit=limit)
|
|
||||||
total = tag_crud.get_tags_count(db)
|
|
||||||
|
|
||||||
return TagListResponse(items=tags, total=total)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", response_model=Tag, status_code=status.HTTP_201_CREATED)
|
|
||||||
def create_tag(tag: TagCreate, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Create a new tag.
|
|
||||||
"""
|
|
||||||
# Check if tag with this name already exists
|
|
||||||
db_tag = tag_crud.get_tag_by_name(db, name=tag.name)
|
|
||||||
if db_tag:
|
|
||||||
raise HTTPException(status_code=400, detail="Tag with this name already exists")
|
|
||||||
|
|
||||||
return tag_crud.create_tag(db=db, tag=tag)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tag_id}", response_model=Tag)
|
|
||||||
def read_tag(tag_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Get a specific tag by ID.
|
|
||||||
"""
|
|
||||||
db_tag = tag_crud.get_tag(db, tag_id=tag_id)
|
|
||||||
if db_tag is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
return db_tag
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{tag_id}", response_model=Tag)
|
|
||||||
def update_tag(tag_id: int, tag: TagUpdate, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Update a tag.
|
|
||||||
"""
|
|
||||||
# Check if tag exists
|
|
||||||
db_tag = tag_crud.get_tag(db, tag_id=tag_id)
|
|
||||||
if db_tag is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
# Check if new name conflicts with existing tag
|
|
||||||
if tag.name and tag.name != db_tag.name:
|
|
||||||
existing_tag = tag_crud.get_tag_by_name(db, name=tag.name)
|
|
||||||
if existing_tag:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400, detail="Tag with this name already exists"
|
|
||||||
)
|
|
||||||
|
|
||||||
return tag_crud.update_tag(db=db, tag_id=tag_id, tag=tag)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{tag_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def delete_tag(tag_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Delete a tag.
|
|
||||||
"""
|
|
||||||
success = tag_crud.delete_tag(db, tag_id=tag_id)
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{tag_id}/todos/{todo_id}", status_code=status.HTTP_201_CREATED)
|
|
||||||
def add_tag_to_todo(tag_id: int, todo_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Add a tag to a todo.
|
|
||||||
"""
|
|
||||||
success = tag_crud.add_tag_to_todo(db, todo_id=todo_id, tag_id=tag_id)
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Failed to add tag to todo. Tag or todo not found, or tag already assigned.",
|
|
||||||
)
|
|
||||||
return {"message": "Tag successfully added to todo"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{tag_id}/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
def remove_tag_from_todo(tag_id: int, todo_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Remove a tag from a todo.
|
|
||||||
"""
|
|
||||||
success = tag_crud.remove_tag_from_todo(db, todo_id=todo_id, tag_id=tag_id)
|
|
||||||
if not success:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Failed to remove tag from todo. Tag or todo not found, or tag not assigned.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{tag_id}/todos", response_model=List[Todo])
|
|
||||||
def get_todos_by_tag(
|
|
||||||
tag_id: int, skip: int = 0, limit: int = 100, db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Get all todos that have a specific tag.
|
|
||||||
"""
|
|
||||||
# Check if tag exists
|
|
||||||
db_tag = tag_crud.get_tag(db, tag_id=tag_id)
|
|
||||||
if db_tag is None:
|
|
||||||
raise HTTPException(status_code=404, detail="Tag not found")
|
|
||||||
|
|
||||||
return tag_crud.get_todos_by_tag(db, tag_id=tag_id, skip=skip, limit=limit)
|
|
@ -1,92 +0,0 @@
|
|||||||
from typing import List, Optional
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy import func
|
|
||||||
|
|
||||||
from app.models.tag import Tag
|
|
||||||
from app.models.todo import Todo
|
|
||||||
from app.schemas.tag import TagCreate, TagUpdate
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag(db: Session, tag_id: int) -> Optional[Tag]:
|
|
||||||
"""Get a single tag by ID."""
|
|
||||||
return db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag_by_name(db: Session, name: str) -> Optional[Tag]:
|
|
||||||
"""Get a tag by name."""
|
|
||||||
return db.query(Tag).filter(Tag.name == name).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tags(db: Session, skip: int = 0, limit: int = 100) -> List[Tag]:
|
|
||||||
"""Get multiple tags with pagination."""
|
|
||||||
return db.query(Tag).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tags_count(db: Session) -> int:
|
|
||||||
"""Get total count of tags."""
|
|
||||||
return db.query(func.count(Tag.id)).scalar()
|
|
||||||
|
|
||||||
|
|
||||||
def create_tag(db: Session, tag: TagCreate) -> Tag:
|
|
||||||
"""Create a new tag."""
|
|
||||||
db_tag = Tag(name=tag.name, color=tag.color)
|
|
||||||
db.add(db_tag)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_tag)
|
|
||||||
return db_tag
|
|
||||||
|
|
||||||
|
|
||||||
def update_tag(db: Session, tag_id: int, tag: TagUpdate) -> Optional[Tag]:
|
|
||||||
"""Update an existing tag."""
|
|
||||||
db_tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if db_tag:
|
|
||||||
update_data = tag.model_dump(exclude_unset=True)
|
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(db_tag, field, value)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(db_tag)
|
|
||||||
return db_tag
|
|
||||||
|
|
||||||
|
|
||||||
def delete_tag(db: Session, tag_id: int) -> bool:
|
|
||||||
"""Delete a tag."""
|
|
||||||
db_tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if db_tag:
|
|
||||||
db.delete(db_tag)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def add_tag_to_todo(db: Session, todo_id: int, tag_id: int) -> bool:
|
|
||||||
"""Add a tag to a todo."""
|
|
||||||
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
|
|
||||||
if todo and tag and tag not in todo.tags:
|
|
||||||
todo.tags.append(tag)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def remove_tag_from_todo(db: Session, todo_id: int, tag_id: int) -> bool:
|
|
||||||
"""Remove a tag from a todo."""
|
|
||||||
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
|
|
||||||
if todo and tag and tag in todo.tags:
|
|
||||||
todo.tags.remove(tag)
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_todos_by_tag(
|
|
||||||
db: Session, tag_id: int, skip: int = 0, limit: int = 100
|
|
||||||
) -> List[Todo]:
|
|
||||||
"""Get all todos that have a specific tag."""
|
|
||||||
tag = db.query(Tag).filter(Tag.id == tag_id).first()
|
|
||||||
if tag:
|
|
||||||
return tag.todos[skip : skip + limit]
|
|
||||||
return []
|
|
@ -1,17 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, String, DateTime
|
|
||||||
from sqlalchemy.sql import func
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(Base):
|
|
||||||
__tablename__ = "tags"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
name = Column(String(50), nullable=False, unique=True)
|
|
||||||
color = Column(String(7), nullable=False, default="#3B82F6") # Hex color code
|
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
||||||
|
|
||||||
# Many-to-many relationship with todos
|
|
||||||
todos = relationship("Todo", secondary="todo_tags", back_populates="tags")
|
|
@ -1,15 +0,0 @@
|
|||||||
from sqlalchemy import Column, Integer, ForeignKey, Table
|
|
||||||
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
# Association table for many-to-many relationship between todos and tags
|
|
||||||
todo_tags = Table(
|
|
||||||
"todo_tags",
|
|
||||||
Base.metadata,
|
|
||||||
Column(
|
|
||||||
"todo_id", Integer, ForeignKey("todos.id", ondelete="CASCADE"), primary_key=True
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
"tag_id", Integer, ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True
|
|
||||||
),
|
|
||||||
)
|
|
@ -1,36 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
|
|
||||||
class TagBase(BaseModel):
|
|
||||||
name: str = Field(..., min_length=1, max_length=50, description="Tag name")
|
|
||||||
color: str = Field(
|
|
||||||
default="#3B82F6", pattern=r"^#[0-9A-Fa-f]{6}$", description="Hex color code"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TagCreate(TagBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TagUpdate(BaseModel):
|
|
||||||
name: Optional[str] = Field(
|
|
||||||
None, min_length=1, max_length=50, description="Tag name"
|
|
||||||
)
|
|
||||||
color: Optional[str] = Field(
|
|
||||||
None, pattern=r"^#[0-9A-Fa-f]{6}$", description="Hex color code"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(TagBase):
|
|
||||||
id: int
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class TagListResponse(BaseModel):
|
|
||||||
items: list[Tag]
|
|
||||||
total: int
|
|
Loading…
x
Reference in New Issue
Block a user