Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/models/todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqlmodel import Field, SQLModel
from typing import Optional
from datetime import datetime

class TodoBase(SQLModel):
title: str = Field(index=True)
description: Optional[str] = Field(default=None)
completed: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.now)
user_id: int = Field(foreign_key="user.id")

class Todo(TodoBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
1 change: 1 addition & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlmodel import Field, SQLModel
from typing import Optional
from pydantic import EmailStr
from typing import List


class UserBase(SQLModel,):
Expand Down
76 changes: 76 additions & 0 deletions app/repositories/todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from sqlmodel import Session, select, func
from app.models.todo import Todo, TodoBase
from typing import Optional, Tuple, List
from app.utilities.pagination import Pagination
import logging

logger = logging.getLogger(__name__)

class TodoRepository:
def __init__(self, db: Session):
self.db = db

def create(self, todo_data: TodoBase, user_id: int) -> Optional[Todo]:
try:
todo = Todo.model_validate(todo_data)
todo.user_id = user_id
self.db.add(todo)
self.db.commit()
self.db.refresh(todo)
return todo
except Exception as e:
logger.error(f"An error occurred while saving todo: {e}")
self.db.rollback()
raise

def get_by_id(self, todo_id: int, user_id: int) -> Optional[Todo]:
return self.db.exec(
select(Todo).where(Todo.id == todo_id, Todo.user_id == user_id)
).one_or_none()

def get_user_todos(self, user_id: int, page: int = 1, limit: int = 10) -> Tuple[List[Todo], Pagination]:
offset = (page - 1) * limit
db_qry = select(Todo).where(Todo.user_id == user_id).order_by(Todo.created_at.desc())

count_qry = select(func.count()).select_from(db_qry.subquery())
count_todos = self.db.exec(count_qry).one()

todos = self.db.exec(db_qry.offset(offset).limit(limit)).all()
pagination = Pagination(total_count=count_todos, current_page=page, limit=limit)

return todos, pagination

def get_all_user_todos(self, user_id: int) -> List[Todo]:
return self.db.exec(
select(Todo).where(Todo.user_id == user_id).order_by(Todo.created_at.desc())
).all()

def update_todo_status(self, todo_id: int, user_id: int, completed: bool) -> Optional[Todo]:
todo = self.get_by_id(todo_id, user_id)
if not todo:
return None

todo.completed = completed
try:
self.db.add(todo)
self.db.commit()
self.db.refresh(todo)
return todo
except Exception as e:
logger.error(f"An error occurred while updating todo: {e}")
self.db.rollback()
raise

def delete_todo(self, todo_id: int, user_id: int) -> bool:
todo = self.get_by_id(todo_id, user_id)
if not todo:
return False

try:
self.db.delete(todo)
self.db.commit()
return True
except Exception as e:
logger.error(f"An error occurred while deleting todo: {e}")
self.db.rollback()
raise
2 changes: 1 addition & 1 deletion app/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
router = APIRouter(tags=["Jinja Based Endpoints"], include_in_schema=get_settings().env.lower() in ["dev","development"])
api_router = APIRouter(tags=["API Endpoints"], prefix="/api")

from . import (index, login, register, admin_home, user_home, users, logout)
from . import (index, login, register, admin_home, user_home, users, todos, logout)
59 changes: 59 additions & 0 deletions app/routers/todos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fastapi import Request, Depends, HTTPException, status
from app.dependencies import SessionDep
from app.dependencies.auth import AuthDep
from . import api_router
from app.services.todo_service import TodoService
from app.repositories.todo import TodoRepository
from app.schemas.todo import TodoCreate, TodoResponse
from typing import List


@api_router.get("/todos", response_model=List[TodoResponse])
async def get_user_todos(request: Request, db: SessionDep, user: AuthDep):
todo_repo = TodoRepository(db)
todo_service = TodoService(todo_repo)
todos = todo_service.get_user_todos(user.id)
return todos


@api_router.post("/todos", response_model=TodoResponse, status_code=status.HTTP_201_CREATED)
async def create_todo(
todo_data: TodoCreate,
db: SessionDep,
user: AuthDep
):
todo_repo = TodoRepository(db)
todo_service = TodoService(todo_repo)

todo = todo_service.create_todo(todo_data, user.id)
return todo


@api_router.post("/todos/{todo_id}/toggle", response_model=TodoResponse)
async def toggle_todo(
todo_id: int,
db: SessionDep,
user: AuthDep
):
todo_repo = TodoRepository(db)
todo_service = TodoService(todo_repo)

todo = todo_service.toggle_todo_status(todo_id, user.id)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo


@api_router.delete("/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_todo(
todo_id: int,
db: SessionDep,
user: AuthDep
):
todo_repo = TodoRepository(db)
todo_service = TodoService(todo_repo)

deleted = todo_service.delete_todo(todo_id, user.id)
if not deleted:
raise HTTPException(status_code=404, detail="Todo not found")
return None
16 changes: 16 additions & 0 deletions app/routers/user_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from app.dependencies.session import SessionDep
from app.dependencies.auth import AuthDep, IsUserLoggedIn, get_current_user, is_admin
from . import router, templates
from app.repositories.todo import TodoRepository
from app.services.todo_service import TodoService


@router.get("/app", response_class=HTMLResponse)
Expand All @@ -18,4 +20,18 @@ async def user_home_view(
context={
"user": user
}
)

@router.get("/todos", response_class=HTMLResponse)
async def todos_view(
request: Request,
user: AuthDep,
db: SessionDep
):
return templates.TemplateResponse(
request=request,
name="todos.html",
context={
"user": user
}
)
18 changes: 18 additions & 0 deletions app/schemas/todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional

class TodoCreate(BaseModel):
title: str
description: Optional[str] = None

class TodoResponse(BaseModel):
id: int
title: str
description: Optional[str]
completed: bool
created_at: datetime
user_id: int

class TodoUpdate(BaseModel):
completed: bool
32 changes: 32 additions & 0 deletions app/services/todo_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from app.repositories.todo import TodoRepository
from app.models.todo import TodoBase, Todo
from app.schemas.todo import TodoCreate
from typing import List, Optional
from datetime import datetime

class TodoService:
def __init__(self, todo_repo: TodoRepository):
self.todo_repo = todo_repo

def create_todo(self, todo_data: TodoCreate, user_id: int) -> Todo:
# Convert TodoCreate to TodoBase (without user_id)
todo_base = TodoBase(
title=todo_data.title,
description=todo_data.description,
completed=False,
created_at=datetime.now(),
user_id=user_id # Add user_id here!
)
return self.todo_repo.create(todo_base, user_id)

def get_user_todos(self, user_id: int) -> List[Todo]:
return self.todo_repo.get_all_user_todos(user_id)

def toggle_todo_status(self, todo_id: int, user_id: int) -> Optional[Todo]:
todo = self.todo_repo.get_by_id(todo_id, user_id)
if not todo:
return None
return self.todo_repo.update_todo_status(todo_id, user_id, not todo.completed)

def delete_todo(self, todo_id: int, user_id: int) -> bool:
return self.todo_repo.delete_todo(todo_id, user_id)
4 changes: 2 additions & 2 deletions app/templates/authenticated-base.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ <h4 class="text-white mb-4 text-center py-4">Project Template</h4>
<ul class="nav nav-pills flex-column gap-2 flex-grow-1">
<li class="nav-item">
<a class="nav-link d-flex align-items-center {{ 'active' if request.path == '/app' }}" href="/app">
<span class="material-symbols-outlined me-2">people</span>
Users
<span class="material-symbols-outlined me-2">checklist</span>
Todos
</a>
</li>

Expand Down
Loading