From 861e9e9bc86c1c6cfe3cff26147cbf5c4b86ca07 Mon Sep 17 00:00:00 2001 From: Macey Ragbir Date: Wed, 15 Apr 2026 03:01:21 -0400 Subject: [PATCH 1/2] requirements.txt file added to the repository. --- requirements.txt | 203 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..57a9cd1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,203 @@ +# +# This file is autogenerated by pip-compile with Python 3.14 +# by the following command: +# +# pip-compile --output-file=requirements.txt pyproject.toml +# +annotated-doc==0.0.4 + # via + # fastapi + # typer +annotated-types==0.7.0 + # via pydantic +anyio==4.13.0 + # via + # httpx + # starlette + # watchfiles +argon2-cffi==25.1.0 + # via pwdlib +argon2-cffi-bindings==25.1.0 + # via argon2-cffi +asyncpg==0.31.0 + # via project-starter (pyproject.toml) +certifi==2026.2.25 + # via + # httpcore + # httpx + # sentry-sdk +cffi==2.0.0 + # via argon2-cffi-bindings +click==8.3.2 + # via + # rich-toolkit + # typer + # uvicorn +colorama==0.4.6 + # via + # click + # pytest + # uvicorn +dnspython==2.8.0 + # via email-validator +ecdsa==0.19.2 + # via python-jose +email-validator==2.3.0 + # via + # fastapi + # pydantic +fastapi[all]==0.135.3 + # via project-starter (pyproject.toml) +fastapi-cli[standard]==0.0.24 + # via fastapi +fastapi-cloud-cli==0.16.1 + # via fastapi-cli +fastar==0.11.0 + # via fastapi-cloud-cli +greenlet==3.4.0 + # via sqlalchemy +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httptools==0.7.1 + # via uvicorn +httpx==0.28.1 + # via + # fastapi + # fastapi-cloud-cli + # project-starter (pyproject.toml) +idna==3.11 + # via + # anyio + # email-validator + # httpx +iniconfig==2.3.0 + # via pytest +itsdangerous==2.2.0 + # via + # fastapi + # project-starter (pyproject.toml) +jinja2==3.1.6 + # via + # fastapi + # project-starter (pyproject.toml) +markdown-it-py==4.0.0 + # via rich +markupsafe==3.0.3 + # via jinja2 +mdurl==0.1.2 + # via markdown-it-py +packaging==26.1 + # via pytest +pluggy==1.6.0 + # via pytest +psycopg2-binary==2.9.11 + # via project-starter (pyproject.toml) +pwdlib[argon2]==0.3.0 + # via project-starter (pyproject.toml) +pyasn1==0.6.3 + # via + # python-jose + # rsa +pycparser==3.0 + # via cffi +pydantic[email]==2.13.0 + # via + # fastapi + # fastapi-cloud-cli + # pydantic-extra-types + # pydantic-settings + # sqlmodel +pydantic-core==2.46.0 + # via pydantic +pydantic-extra-types==2.11.1 + # via fastapi +pydantic-settings==2.13.1 + # via fastapi +pygments==2.20.0 + # via + # pytest + # rich +pyjwt==2.12.1 + # via project-starter (pyproject.toml) +pytest==9.0.3 + # via + # project-starter (pyproject.toml) + # pytest-asyncio +pytest-asyncio==1.3.0 + # via project-starter (pyproject.toml) +python-dotenv==1.2.2 + # via + # pydantic-settings + # uvicorn +python-jose==3.5.0 + # via project-starter (pyproject.toml) +python-multipart==0.0.26 + # via fastapi +pyyaml==6.0.3 + # via + # fastapi + # uvicorn +rich==15.0.0 + # via + # rich-toolkit + # typer +rich-toolkit==0.19.7 + # via + # fastapi-cli + # fastapi-cloud-cli +rignore==0.7.6 + # via fastapi-cloud-cli +rsa==4.9.1 + # via python-jose +ruff==0.15.10 + # via project-starter (pyproject.toml) +sentry-sdk==2.58.0 + # via fastapi-cloud-cli +shellingham==1.5.4 + # via typer +six==1.17.0 + # via ecdsa +sqlalchemy==2.0.49 + # via sqlmodel +sqlmodel==0.0.38 + # via project-starter (pyproject.toml) +starlette==1.0.0 + # via fastapi +tabulate==0.10.0 + # via project-starter (pyproject.toml) +typer==0.24.1 + # via + # fastapi-cli + # fastapi-cloud-cli + # project-starter (pyproject.toml) +typing-extensions==4.15.0 + # via + # fastapi + # pydantic + # pydantic-core + # pydantic-extra-types + # rich-toolkit + # sqlalchemy + # sqlmodel + # typing-inspection +typing-inspection==0.4.2 + # via + # fastapi + # pydantic + # pydantic-settings +urllib3==2.6.3 + # via sentry-sdk +uvicorn[standard]==0.44.0 + # via + # fastapi + # fastapi-cli + # fastapi-cloud-cli + # project-starter (pyproject.toml) +watchfiles==1.1.1 + # via uvicorn +websockets==16.0 + # via uvicorn From e2e3f9be47b439a2f7ac4daaf27826fd1cfd187d Mon Sep 17 00:00:00 2001 From: Macey Ragbir Date: Wed, 15 Apr 2026 23:20:56 -0400 Subject: [PATCH 2/2] Lab#7 Exercise --- app/models/todo.py | 13 ++ app/models/user.py | 1 + app/repositories/todo.py | 76 +++++++++++ app/routers/__init__.py | 2 +- app/routers/todos.py | 59 +++++++++ app/routers/user_home.py | 16 +++ app/schemas/todo.py | 18 +++ app/services/todo_service.py | 32 +++++ app/templates/authenticated-base.html | 4 +- app/templates/todos.html | 176 ++++++++++++++++++++++++++ 10 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 app/models/todo.py create mode 100644 app/repositories/todo.py create mode 100644 app/routers/todos.py create mode 100644 app/schemas/todo.py create mode 100644 app/services/todo_service.py create mode 100644 app/templates/todos.html 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