diff --git a/app/models/todo.py b/app/models/todo.py new file mode 100644 index 0000000..208baec --- /dev/null +++ b/app/models/todo.py @@ -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) \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index af5c517..fc75c5b 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,6 +1,7 @@ from sqlmodel import Field, SQLModel from typing import Optional from pydantic import EmailStr +from typing import List class UserBase(SQLModel,): diff --git a/app/repositories/todo.py b/app/repositories/todo.py new file mode 100644 index 0000000..29f0e26 --- /dev/null +++ b/app/repositories/todo.py @@ -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 \ No newline at end of file diff --git a/app/routers/__init__.py b/app/routers/__init__.py index f9388f7..6e5104b 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -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) \ No newline at end of file +from . import (index, login, register, admin_home, user_home, users, todos, logout) \ No newline at end of file diff --git a/app/routers/todos.py b/app/routers/todos.py new file mode 100644 index 0000000..0dc4249 --- /dev/null +++ b/app/routers/todos.py @@ -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 \ No newline at end of file diff --git a/app/routers/user_home.py b/app/routers/user_home.py index 6910201..ab3d4c6 100644 --- a/app/routers/user_home.py +++ b/app/routers/user_home.py @@ -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) @@ -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 + } ) \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py new file mode 100644 index 0000000..411d500 --- /dev/null +++ b/app/schemas/todo.py @@ -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 \ No newline at end of file diff --git a/app/services/todo_service.py b/app/services/todo_service.py new file mode 100644 index 0000000..7783df1 --- /dev/null +++ b/app/services/todo_service.py @@ -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) \ No newline at end of file diff --git a/app/templates/authenticated-base.html b/app/templates/authenticated-base.html index 8c7aad6..a793963 100644 --- a/app/templates/authenticated-base.html +++ b/app/templates/authenticated-base.html @@ -74,8 +74,8 @@

Project Template