From a6f496864fcead0a4b0d0fd8b1fef9394e05c48e Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Fri, 23 May 2025 22:19:17 +0200 Subject: [PATCH 01/30] add sharing logic --- service/app/models/plan.py | 2 ++ service/app/routers/plan_router.py | 13 ++++++++++++- service/app/schemas/plan.py | 1 + service/app/services/plan_service.py | 18 +++++++++++++++++- service/pyproject.toml | 2 +- service/tests/conftest.py | 7 ++++++- service/tests/test_plan_router.py | 27 +++++++++++++++++++++++---- 7 files changed, 62 insertions(+), 8 deletions(-) diff --git a/service/app/models/plan.py b/service/app/models/plan.py index c130030..dd19ff8 100644 --- a/service/app/models/plan.py +++ b/service/app/models/plan.py @@ -1,5 +1,6 @@ import uuid from datetime import datetime +from typing import Optional from sqlmodel import Field, SQLModel @@ -9,6 +10,7 @@ class Plan(SQLModel, table=True): group_version_id: uuid.UUID = Field(default_factory=uuid.uuid4) name: str = Field(nullable=False) content: str + public_slug: Optional[str] is_favorite: bool = Field(default=False) created_at: datetime = Field(default_factory=datetime.now) user_id: uuid.UUID = Field(nullable=False, foreign_key="user.id") diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 35662b7..e4df7a4 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -20,10 +20,21 @@ async def get_plans( try: plans = plan_service.get_plans(request.state.user.id, session) except Exception as e: - raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + raise HTTPException(status_code=500, detail="Failed to get plans. Please report to page-admin") from e return {"plans": plans} +@router.get("/plans/{public_slug}") +async def get_plan_by_public_slug(public_slug: str, session: Annotated[Session, Depends(get_session)]) -> PlanRead: + try: + plan = plan_service.get_plan_by_public_slug(public_slug, session) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to get plan. Please report to page-admin") from e + return plan + + @router.post("/plans", dependencies=[Depends(auth_dependency)], status_code=201) async def create_plan( request: Request, plan_data: PlanCreate, session: Annotated[Session, Depends(get_session)] diff --git a/service/app/schemas/plan.py b/service/app/schemas/plan.py index 2f21ab4..6be2303 100644 --- a/service/app/schemas/plan.py +++ b/service/app/schemas/plan.py @@ -9,6 +9,7 @@ class PlanRead(SQLModel): group_version_id: UUID name: str content: str + public_slug: str is_favorite: bool created_at: datetime user_id: UUID diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index c18457f..d25946b 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -1,3 +1,4 @@ +import base64 from collections.abc import Sequence from uuid import UUID @@ -14,10 +15,20 @@ def get_plans(user_id: UUID, session: Session) -> Sequence[PlanRead]: return [PlanRead.model_validate(plan) for plan in plans] +def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: + statement = select(Plan).where(Plan.public_slug == public_slug) + plan = session.exec(statement).first() + if not plan: + error_msg = "Plan not found" + raise ValueError(error_msg) + return PlanRead.model_validate(plan) + + def write_plan(user_id: UUID, plan_data: PlanCreate, session: Session) -> PlanRead: - plan_dict = plan_data.model_dump() # Convert PlanCreate to a dictionary + plan_dict = plan_data.model_dump() plan_dict["user_id"] = user_id created_plan = Plan(**plan_dict) + created_plan.public_slug = create_public_slug(created_plan.group_version_id) session.add(created_plan) session.commit() session.refresh(created_plan) @@ -31,3 +42,8 @@ def delete_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: raise ValueError(error_msg) session.delete(plan) session.commit() + + +def create_public_slug(group_id: UUID) -> str: + slug = base64.urlsafe_b64encode(group_id.bytes).rstrip(b'=').decode('ascii') + return slug diff --git a/service/pyproject.toml b/service/pyproject.toml index 0dad8ce..44a0df5 100644 --- a/service/pyproject.toml +++ b/service/pyproject.toml @@ -43,4 +43,4 @@ skip_empty = true omit = [ "app/middlewares/*", "app/create_tables.py", -] \ No newline at end of file +] diff --git a/service/tests/conftest.py b/service/tests/conftest.py index 7ec43d2..c1e92e1 100644 --- a/service/tests/conftest.py +++ b/service/tests/conftest.py @@ -4,6 +4,7 @@ import pytest from fastapi import Depends, Request from fastapi.testclient import TestClient +from sqlalchemy import StaticPool from sqlalchemy.exc import OperationalError from sqlalchemy.orm import sessionmaker from sqlmodel import Session, create_engine @@ -28,7 +29,11 @@ async def override_auth_dependency(request: Request, session: Annotated[Session, def override_get_session() -> Generator[Session, Any]: - engine = create_engine("sqlite:///:memory:") + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool + ) session = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=Session) db = session() diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 195462d..99b6075 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -49,10 +49,6 @@ def test_delete_plan(self, test_client: TestClient) -> None: assert len(response.json()["plans"]) == 0 def test_delete_nonexisting_plan(self, test_client: TestClient) -> None: - response = test_client.get("/plans") - assert response.status_code == 200 - assert len(response.json()["plans"]) == 0 - response = test_client.delete(f"/plans/{uuid.uuid4()}") assert response.status_code == 404 @@ -63,6 +59,10 @@ def test_get_plans(self, test_client: TestClient) -> None: response = test_client.get("/plans") assert response.status_code == 500 + def test_get_plan(self, test_client: TestClient) -> None: + response = test_client.get("/plans/testSlug") + assert response.status_code == 500 + def test_create_plan(self, test_client: TestClient) -> None: request_data = {"name": "Test Plan", "content": "Test Content"} @@ -72,3 +72,22 @@ def test_create_plan(self, test_client: TestClient) -> None: def test_delete_plan(self, test_client: TestClient) -> None: response = test_client.delete(f"/plans/{uuid.uuid4()}") assert response.status_code == 500 + + +class TestPublicSlug: + def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} + + response = test_client.post("/plans", json=request_data) + assert response.status_code == 201 + public_slug = response.json()["public_slug"] + + response = test_client.get(f"/plans/{public_slug}") + assert response.status_code == 200 + data = response.json() + assert data["name"] == request_data["name"] + assert data["content"] == request_data["content"] + + def test_get_nonexisting_plan(self, test_client: TestClient) -> None: + response = test_client.get(f"/plans/SLUG") + assert response.status_code == 404 \ No newline at end of file From 5adc29aa07b94c607e5872b1635bcbbc27a892a1 Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sat, 24 May 2025 12:23:19 +0200 Subject: [PATCH 02/30] updated backend --- service/app/routers/plan_router.py | 2 +- service/tests/test_plan_router.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index e4df7a4..c6fa822 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -24,7 +24,7 @@ async def get_plans( return {"plans": plans} -@router.get("/plans/{public_slug}") +@router.get("/plans/shared/{public_slug}") async def get_plan_by_public_slug(public_slug: str, session: Annotated[Session, Depends(get_session)]) -> PlanRead: try: plan = plan_service.get_plan_by_public_slug(public_slug, session) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 99b6075..496ae47 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -60,7 +60,7 @@ def test_get_plans(self, test_client: TestClient) -> None: assert response.status_code == 500 def test_get_plan(self, test_client: TestClient) -> None: - response = test_client.get("/plans/testSlug") + response = test_client.get("/plans/shared/testSlug") assert response.status_code == 500 def test_create_plan(self, test_client: TestClient) -> None: @@ -82,12 +82,12 @@ def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: assert response.status_code == 201 public_slug = response.json()["public_slug"] - response = test_client.get(f"/plans/{public_slug}") + response = test_client.get(f"/plans/shared/{public_slug}") assert response.status_code == 200 data = response.json() assert data["name"] == request_data["name"] assert data["content"] == request_data["content"] def test_get_nonexisting_plan(self, test_client: TestClient) -> None: - response = test_client.get(f"/plans/SLUG") + response = test_client.get(f"/plans/shared/testSlug") assert response.status_code == 404 \ No newline at end of file From 7363620ae7448498c9abc8d2bf2f1814b67becbe Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sun, 25 May 2025 12:16:50 +0200 Subject: [PATCH 03/30] poc history logic --- service/app/routers/plan_router.py | 19 +++++++- service/app/schemas/plan.py | 3 ++ service/app/services/plan_service.py | 67 ++++++++++++++++++++++++---- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index c6fa822..e30bd73 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -4,10 +4,11 @@ from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel import Session +from uvloop.dns import request from ..database import get_session from ..middlewares.auth_middleware import auth_dependency -from ..schemas.plan import PlanCreate, PlanRead +from ..schemas.plan import PlanCreate, PlanRead, PlanUpdate from ..services import plan_service router = APIRouter() @@ -34,6 +35,13 @@ async def get_plan_by_public_slug(public_slug: str, session: Annotated[Session, raise HTTPException(status_code=500, detail="Failed to get plan. Please report to page-admin") from e return plan +@router.get("/plans/history/{plan_id}") +async def get_plan_history(plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> dict[str, Sequence[PlanRead]]: + try: + plans = plan_service.get_plan_history(request.state.user.id, plan_id, session) + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to get plans. Please report to page-admin") from e + return {"plans": plans} @router.post("/plans", dependencies=[Depends(auth_dependency)], status_code=201) async def create_plan( @@ -45,6 +53,15 @@ async def create_plan( raise HTTPException(status_code=500, detail="Failed to create plan. Please report to page-admin") from e return created_plan +@router.post("/plans/{plan_id}/update", dependencies=[Depends(auth_dependency)], status_code=201) +async def update_plan( + request: Request, plan_id: UUID, plan_data: PlanUpdate, session: Annotated[Session, Depends(get_session)] +) -> PlanRead: + try: + created_plan = plan_service.update_plan(request.state.user.id, plan_id, plan_data, session) + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to create plan. Please report to page-admin") from e + return created_plan @router.delete("/plans/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: diff --git a/service/app/schemas/plan.py b/service/app/schemas/plan.py index 6be2303..7d05db6 100644 --- a/service/app/schemas/plan.py +++ b/service/app/schemas/plan.py @@ -18,3 +18,6 @@ class PlanRead(SQLModel): class PlanCreate(SQLModel): name: str content: str + +class PlanUpdate(SQLModel): + content: str diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index d25946b..7dd2d5d 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -3,20 +3,41 @@ from uuid import UUID from sqlmodel import Session, select +from sqlalchemy import func, and_ from ..models.plan import Plan -from ..schemas.plan import PlanCreate, PlanRead +from ..schemas.plan import PlanCreate, PlanRead, PlanUpdate def get_plans(user_id: UUID, session: Session) -> Sequence[PlanRead]: - statement = select(Plan).where(Plan.user_id == user_id) - + subquery = ( + select( + Plan.group_version_id, + func.max(Plan.created_at).label("max_created_at") + ) + .where(Plan.user_id == user_id) + .group_by(Plan.group_version_id) + .subquery() + ) + statement = ( + select(Plan) + .join(subquery, + and_( + Plan.group_version_id == subquery.c.group_version_id, + Plan.created_at == subquery.c.max_created_at + )) + .where(Plan.user_id == user_id) + ) plans = session.exec(statement).all() return [PlanRead.model_validate(plan) for plan in plans] def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: - statement = select(Plan).where(Plan.public_slug == public_slug) + statement = ( + select(Plan) + .where(Plan.public_slug == public_slug) + .order_by(Plan.created_at.desc()) + ) plan = session.exec(statement).first() if not plan: error_msg = "Plan not found" @@ -24,6 +45,17 @@ def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: return PlanRead.model_validate(plan) +def get_plan_history(user_id: UUID, plan_id: UUID, session: Session) -> Sequence[PlanRead]: + current_plan = session.get(Plan, plan_id) + statement = ( + select(Plan) + .where(Plan.group_version_id == current_plan.group_version_id) + .order_by(Plan.created_at.desc()) + ) + plans = session.exec(statement).all() + return [PlanRead.model_validate(plan) for plan in plans] + + def write_plan(user_id: UUID, plan_data: PlanCreate, session: Session) -> PlanRead: plan_dict = plan_data.model_dump() plan_dict["user_id"] = user_id @@ -36,14 +68,33 @@ def write_plan(user_id: UUID, plan_data: PlanCreate, session: Session) -> PlanRe def delete_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: - plan = session.get(Plan, plan_id) - if not plan or plan.user_id != user_id: + current_plan = session.get(Plan, plan_id) + if not current_plan or current_plan.user_id != user_id: error_msg = "Plan not found or access denied" raise ValueError(error_msg) - session.delete(plan) - session.commit() + statement = select(Plan).where(Plan.group_version_id == current_plan.group_version_id) + plan_group = session.exec(statement).all() + for plan in plan_group: + session.delete(plan) + session.commit() def create_public_slug(group_id: UUID) -> str: slug = base64.urlsafe_b64encode(group_id.bytes).rstrip(b'=').decode('ascii') return slug + +def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Session) -> PlanRead: + current_plan = session.get(Plan, plan_id) + new_plan_data = { + "group_version_id": current_plan.group_version_id, + "name": current_plan.name, + "content": plan_data.content, + "public_slug": current_plan.public_slug, + "is_favorite": current_plan.is_favorite, + "user_id": user_id + } + new_plan = Plan(**new_plan_data) + session.add(new_plan) + session.commit() + session.refresh(new_plan) + return PlanRead.model_validate(new_plan) \ No newline at end of file From e9a75fdad1c6d4e3cf3cafdb2a00cbe679dfab18 Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Mon, 26 May 2025 09:12:07 +0200 Subject: [PATCH 04/30] added tests for history feature --- service/app/routers/plan_router.py | 7 +++-- service/tests/test_plan_router.py | 48 ++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index e30bd73..62ee216 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel import Session -from uvloop.dns import request from ..database import get_session from ..middlewares.auth_middleware import auth_dependency @@ -35,8 +34,10 @@ async def get_plan_by_public_slug(public_slug: str, session: Annotated[Session, raise HTTPException(status_code=500, detail="Failed to get plan. Please report to page-admin") from e return plan -@router.get("/plans/history/{plan_id}") -async def get_plan_history(plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> dict[str, Sequence[PlanRead]]: +@router.get("/plans/history/{plan_id}", dependencies=[Depends(auth_dependency)]) +async def get_plan_history( + request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)] +) -> dict[str, Sequence[PlanRead]]: try: plans = plan_service.get_plan_history(request.state.user.id, plan_id, session) except Exception as e: diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 496ae47..8aadda3 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,5 +1,4 @@ import uuid - import pytest from fastapi.testclient import TestClient @@ -73,6 +72,16 @@ def test_delete_plan(self, test_client: TestClient) -> None: response = test_client.delete(f"/plans/{uuid.uuid4()}") assert response.status_code == 500 + def test_update_plan(self, test_client: TestClient) -> None: + request_data = {"content": "Test Content"} + + response = test_client.post(f"/plans/{uuid.uuid4()}/update", json=request_data) + assert response.status_code == 500 + + def test_get_plan_history(self, test_client: TestClient) -> None: + response = test_client.get(f"/plans/history/{uuid.uuid4()}") + assert response.status_code == 500 + class TestPublicSlug: def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: @@ -90,4 +99,39 @@ def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: def test_get_nonexisting_plan(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/shared/testSlug") - assert response.status_code == 404 \ No newline at end of file + assert response.status_code == 404 + + +class TestPlanHistory: + def test_update_plan(self, test_client: TestClient) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} + response = test_client.post("/plans", json=request_data) + assert response.status_code == 201 + + response = test_client.get("/plans") + assert response.status_code == 200 + data = response.json() + assert len(data["plans"]) == 1 + plan_id = data["plans"][0]["id"] + + request_data = {"content": "New Test Content"} + response = test_client.post(f"/plans/{plan_id}/update", json=request_data) + assert response.status_code == 201 + data = response.json() + assert data["content"] == request_data["content"] + + def test_get_plan_history(self, test_client: TestClient) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} + response = test_client.post("/plans", json=request_data) + assert response.status_code == 201 + plan_id = response.json()["id"] + + request_data = {"content": "New Test Content"} + response = test_client.post(f"/plans/{plan_id}/update", json=request_data) + assert response.status_code == 201 + plan_id = response.json()["id"] + + response = test_client.get(f"/plans/history/{plan_id}") + assert response.status_code == 200 + data = response.json() + assert len(data["plans"]) == 2 \ No newline at end of file From 7be1df8fd056ce66cc428c1e2e223113870825f0 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Tue, 27 May 2025 14:26:03 +0200 Subject: [PATCH 05/30] use bookmark instead of is_favorite --- service/app/models/plan.py | 2 +- service/app/schemas/plan.py | 2 +- service/app/services/plan_service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/app/models/plan.py b/service/app/models/plan.py index dd19ff8..21083a5 100644 --- a/service/app/models/plan.py +++ b/service/app/models/plan.py @@ -11,6 +11,6 @@ class Plan(SQLModel, table=True): name: str = Field(nullable=False) content: str public_slug: Optional[str] - is_favorite: bool = Field(default=False) + bookmark: bool = Field(default=False) created_at: datetime = Field(default_factory=datetime.now) user_id: uuid.UUID = Field(nullable=False, foreign_key="user.id") diff --git a/service/app/schemas/plan.py b/service/app/schemas/plan.py index 7d05db6..a57c86c 100644 --- a/service/app/schemas/plan.py +++ b/service/app/schemas/plan.py @@ -10,7 +10,7 @@ class PlanRead(SQLModel): name: str content: str public_slug: str - is_favorite: bool + bookmark: bool created_at: datetime user_id: UUID diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 7dd2d5d..7d12bf2 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -90,7 +90,7 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se "name": current_plan.name, "content": plan_data.content, "public_slug": current_plan.public_slug, - "is_favorite": current_plan.is_favorite, + "bookmark": current_plan.bookmark, "user_id": user_id } new_plan = Plan(**new_plan_data) From 8769ac932570d9741d67d0929cd39e6a2a788201 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Thu, 15 May 2025 10:30:59 +0200 Subject: [PATCH 06/30] add plan,user import --- service/app/create_tables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index c0b34cc..df2d8b7 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,4 +1,5 @@ from database import create_db_and_tables +from models import plan,user # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 From bc8ff83356f5fe9ae5e29e206b1e7fe839f3c1f2 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Thu, 15 May 2025 11:25:24 +0200 Subject: [PATCH 07/30] start bookmark plans feature --- service/app/services/plan_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 7d12bf2..10f7bb7 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -97,4 +97,4 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se session.add(new_plan) session.commit() session.refresh(new_plan) - return PlanRead.model_validate(new_plan) \ No newline at end of file + return PlanRead.model_validate(new_plan) From 5dcb882d9f6ed8d51c35e7b117e31d2dd312bf22 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 21:51:17 +0200 Subject: [PATCH 08/30] refactor to use bookmark --- service/app/services/plan_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 10f7bb7..b1c241e 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -98,3 +98,8 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se session.commit() session.refresh(new_plan) return PlanRead.model_validate(new_plan) + +def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: + plan = session.get(Plan, plan_id) + + return None From 4ed42195a01728d7266d54ba61b8304ae166a170 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 22:50:25 +0200 Subject: [PATCH 09/30] run linter --- service/app/create_tables.py | 2 +- service/app/routers/plan_router.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index df2d8b7..9b49319 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,5 +1,5 @@ from database import create_db_and_tables -from models import plan,user +from models import plan, user # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 62ee216..72194ec 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -72,3 +72,10 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio raise HTTPException(status_code=404, detail=str(e)) from e except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + +@router.put("/api/plan/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) +async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: + try: + plan_service.bookmark_plan(request.state.user.id, plan_id, session) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e From 6b87fea32463cf4710075ee0866d33dd3f9b833e Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 23:13:16 +0200 Subject: [PATCH 10/30] add tests --- service/tests/test_plan_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 8aadda3..11b6b44 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -134,4 +134,4 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/history/{plan_id}") assert response.status_code == 200 data = response.json() - assert len(data["plans"]) == 2 \ No newline at end of file + assert len(data["plans"]) == 2 From b0097f5f380c47d334a7c314c19b2d6caf6a6857 Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sat, 17 May 2025 17:45:08 +0200 Subject: [PATCH 11/30] update tests --- service/app/create_tables.py | 4 +++- service/app/database.py | 1 + service/app/routers/plan_router.py | 2 ++ service/tests/test_plan_router.py | 3 +++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index 9b49319..e188b80 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,5 +1,7 @@ from database import create_db_and_tables -from models import plan, user + +# Import models so their metadata is registered with SQLModel +from models import plan, user # noqa: F401 # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 diff --git a/service/app/database.py b/service/app/database.py index cd541f9..68bb717 100644 --- a/service/app/database.py +++ b/service/app/database.py @@ -5,6 +5,7 @@ from dotenv import load_dotenv from sqlalchemy.orm import sessionmaker from sqlmodel import Session, SQLModel, create_engine +from sqlalchemy.orm import sessionmaker load_dotenv() diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 72194ec..71357d0 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -79,3 +79,5 @@ async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Sess plan_service.bookmark_plan(request.state.user.id, plan_id, session) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 11b6b44..05eb2cd 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,3 +1,4 @@ +from collections.abc import Iterator import uuid import pytest from fastapi.testclient import TestClient @@ -82,6 +83,8 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/history/{uuid.uuid4()}") assert response.status_code == 500 + def test_create_plan(self, test_client: TestClient, overwrite_session_dependency: Iterator[None]) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} class TestPublicSlug: def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: From 2913817776e97952b72733c976ddc7aa8328761d Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sat, 17 May 2025 17:56:04 +0200 Subject: [PATCH 12/30] fix linting errors --- service/app/database.py | 1 + service/tests/test_plan_router.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/app/database.py b/service/app/database.py index 68bb717..aa79120 100644 --- a/service/app/database.py +++ b/service/app/database.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import sessionmaker from sqlmodel import Session, SQLModel, create_engine from sqlalchemy.orm import sessionmaker +from sqlmodel import Session, SQLModel, create_engine load_dotenv() diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 05eb2cd..6b9351f 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,4 +1,3 @@ -from collections.abc import Iterator import uuid import pytest from fastapi.testclient import TestClient @@ -83,7 +82,7 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/history/{uuid.uuid4()}") assert response.status_code == 500 - def test_create_plan(self, test_client: TestClient, overwrite_session_dependency: Iterator[None]) -> None: + def test_create_plan(self, test_client: TestClient) -> None: request_data = {"name": "Test Plan", "content": "Test Content"} class TestPublicSlug: From 3b992b77323c416e78b4eba699b234f3e8d42ae0 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Thu, 15 May 2025 11:25:24 +0200 Subject: [PATCH 13/30] rebase main into feature/bookmark-plans --- service/app/routers/plan_router.py | 16 ++++++++++++++++ service/app/services/plan_service.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 71357d0..5c18086 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -81,3 +81,19 @@ async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Sess raise HTTPException(status_code=404, detail=str(e)) from e except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + +@router.put("/api/plan/favourite/{plan_id}", dependencies=[Depends(auth_dependency)]) +async def favourite_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: + try: + plan_service.delete_plan(request.state.user.id, plan_id, session) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + +@router.put("/api/plan/favourite/{plan_id}", dependencies=[Depends(auth_dependency)]) +async def favourite_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: + try: + plan_service.delete_plan(request.state.user.id, plan_id, session) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) from e \ No newline at end of file diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index b1c241e..0ebb3fb 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -78,7 +78,6 @@ def delete_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: session.delete(plan) session.commit() - def create_public_slug(group_id: UUID) -> str: slug = base64.urlsafe_b64encode(group_id.bytes).rstrip(b'=').decode('ascii') return slug @@ -103,3 +102,15 @@ def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: plan = session.get(Plan, plan_id) return None + +def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: + plan = session.get(Plan, plan_id) + if not plan or plan.user_id != user_id: + error_msg = "Plan not found or access denied" + raise ValueError(error_msg) + + plan.bookmark = not plan.bookmark + session.add(plan) + session.commit() + session.refresh(plan) + From b43c7f4c4d8879b07b4fd85ca93845c4afe14175 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 21:51:17 +0200 Subject: [PATCH 14/30] refactor to use bookmark --- service/app/routers/plan_router.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 5c18086..815c6d6 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -91,9 +91,9 @@ async def favourite_plan(request: Request, plan_id: UUID, session: Annotated[Ses except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e -@router.put("/api/plan/favourite/{plan_id}", dependencies=[Depends(auth_dependency)]) -async def favourite_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: +@router.put("/api/plan/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) +async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: - plan_service.delete_plan(request.state.user.id, plan_id, session) + print('TODO') except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e \ No newline at end of file From f8cdde6b8751249957b0bef5feb2b5ce337b20ef Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 22:50:25 +0200 Subject: [PATCH 15/30] rebase main into feature/bookmark-plans --- service/app/create_tables.py | 4 ++++ service/app/routers/plan_router.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index e188b80..496b7f5 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,7 +1,11 @@ from database import create_db_and_tables +<<<<<<< HEAD # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 +======= +from models import plan, user +>>>>>>> 504c450 (run linter) # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 815c6d6..f93526b 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -96,4 +96,4 @@ async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Sess try: print('TODO') except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) from e \ No newline at end of file + raise HTTPException(status_code=404, detail=str(e)) from e From ff2c6781f5d588ccb64aafef0c33c66695b49874 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 20:30:39 +0200 Subject: [PATCH 16/30] fixed merge conflicts --- service/app/create_tables.py | 4 ---- service/app/routers/plan_router.py | 13 +++---------- service/app/services/plan_service.py | 7 ++++--- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index 496b7f5..e188b80 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,11 +1,7 @@ from database import create_db_and_tables -<<<<<<< HEAD # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 -======= -from models import plan, user ->>>>>>> 504c450 (run linter) # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index f93526b..27eea4f 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -82,18 +82,11 @@ async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Sess except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e -@router.put("/api/plan/favourite/{plan_id}", dependencies=[Depends(auth_dependency)]) -async def favourite_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: - try: - plan_service.delete_plan(request.state.user.id, plan_id, session) - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) from e - except Exception as e: - raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e - -@router.put("/api/plan/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) +@router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: print('TODO') except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e + except Exception as e: + raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 0ebb3fb..d0dba70 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -105,9 +105,10 @@ def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: plan = session.get(Plan, plan_id) - if not plan or plan.user_id != user_id: - error_msg = "Plan not found or access denied" - raise ValueError(error_msg) + if not plan: + raise ValueError(f"Plan with ID {plan_id} not found.") + if plan.user_id != user_id: + raise ValueError("Access denied: You do not own this plan.") plan.bookmark = not plan.bookmark session.add(plan) From b39296a22002cccefd40375d1565537ec61abfb8 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 20:31:43 +0200 Subject: [PATCH 17/30] fix router --- service/app/routers/plan_router.py | 11 +---------- service/tests/test_plan_router.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 27eea4f..c4af463 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -80,13 +80,4 @@ async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Sess except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) from e except Exception as e: - raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e - -@router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) -async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: - try: - print('TODO') - except ValueError as e: - raise HTTPException(status_code=404, detail=str(e)) from e - except Exception as e: - raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + raise HTTPException(status_code=500, detail="Failed to (un)bookmark plan. Please report to page-admin") from e diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 6b9351f..8eeb3ce 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -51,6 +51,35 @@ def test_delete_nonexisting_plan(self, test_client: TestClient) -> None: response = test_client.delete(f"/plans/{uuid.uuid4()}") assert response.status_code == 404 +class TestBookmarkPlan: + def test_bookmark_plan(self, test_client: TestClient) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} + response = test_client.post("/plans", json=request_data) + assert response.status_code == 201 + plan_id = response.json()["id"] + + response = test_client.patch(f"/plans/bookmark/{plan_id}") + assert response.status_code == 200 + + def test_bookmark_plan_twice(self, test_client: TestClient) -> None: + request_data = {"name": "Test Plan", "content": "Test Content"} + response = test_client.post("/plans", json=request_data) + assert response.status_code == 201 + plan_id = response.json()["id"] + + response = test_client.patch(f"/plans/bookmark/{plan_id}") + assert response.status_code == 200 + response = test_client.patch(f"/plans/bookmark/{plan_id}") + assert response.status_code == 200 + + def test_bookmark_nonexisting_plan(self, test_client: TestClient) -> None: + response = test_client.get("/plans") + assert response.status_code == 200 + assert len(response.json()["plans"]) == 0 + + response = test_client.patch(f"/plans/bookmark/{uuid.uuid4()}") + assert response.status_code == 404 + @pytest.mark.usefixtures("overwrite_session_dependency") class TestBadDB: From fa00c2053f3e8b368a77a755a69112ac4cc37604 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 20:59:58 +0200 Subject: [PATCH 18/30] changed router.patch to return 204 --- service/app/routers/plan_router.py | 2 +- service/app/services/plan_service.py | 5 ----- service/tests/test_plan_router.py | 6 +++--- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index c4af463..87b7b26 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -73,7 +73,7 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e -@router.put("/api/plan/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)]) +@router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: plan_service.bookmark_plan(request.state.user.id, plan_id, session) diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index d0dba70..d124e42 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -98,11 +98,6 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se session.refresh(new_plan) return PlanRead.model_validate(new_plan) -def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: - plan = session.get(Plan, plan_id) - - return None - def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: plan = session.get(Plan, plan_id) if not plan: diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 8eeb3ce..b99a6da 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -59,7 +59,7 @@ def test_bookmark_plan(self, test_client: TestClient) -> None: plan_id = response.json()["id"] response = test_client.patch(f"/plans/bookmark/{plan_id}") - assert response.status_code == 200 + assert response.status_code == 204 def test_bookmark_plan_twice(self, test_client: TestClient) -> None: request_data = {"name": "Test Plan", "content": "Test Content"} @@ -68,9 +68,9 @@ def test_bookmark_plan_twice(self, test_client: TestClient) -> None: plan_id = response.json()["id"] response = test_client.patch(f"/plans/bookmark/{plan_id}") - assert response.status_code == 200 + assert response.status_code == 204 response = test_client.patch(f"/plans/bookmark/{plan_id}") - assert response.status_code == 200 + assert response.status_code == 204 def test_bookmark_nonexisting_plan(self, test_client: TestClient) -> None: response = test_client.get("/plans") From bb3703fb51f342067f66e36a31c68829863a4aa6 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 21:32:56 +0200 Subject: [PATCH 19/30] uv run ruff passing --- service/app/routers/plan_router.py | 1 + service/app/services/plan_service.py | 6 ++++-- service/tests/test_plan_router.py | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 87b7b26..c15d014 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -73,6 +73,7 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + @router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index d124e42..7204b26 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -101,9 +101,11 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: plan = session.get(Plan, plan_id) if not plan: - raise ValueError(f"Plan with ID {plan_id} not found.") + error_msg = f"Plan {plan_id} not found." + raise ValueError(error_msg) if plan.user_id != user_id: - raise ValueError("Access denied: You do not own this plan.") + error_msg = "Plan not found or access denied" + raise ValueError(error_msg) plan.bookmark = not plan.bookmark session.add(plan) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index b99a6da..73a7b9c 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -51,9 +51,10 @@ def test_delete_nonexisting_plan(self, test_client: TestClient) -> None: response = test_client.delete(f"/plans/{uuid.uuid4()}") assert response.status_code == 404 + class TestBookmarkPlan: def test_bookmark_plan(self, test_client: TestClient) -> None: - request_data = {"name": "Test Plan", "content": "Test Content"} + request_data = {"name": "Test Plan", "content": "Test Content"} response = test_client.post("/plans", json=request_data) assert response.status_code == 201 plan_id = response.json()["id"] @@ -62,7 +63,7 @@ def test_bookmark_plan(self, test_client: TestClient) -> None: assert response.status_code == 204 def test_bookmark_plan_twice(self, test_client: TestClient) -> None: - request_data = {"name": "Test Plan", "content": "Test Content"} + request_data = {"name": "Test Plan", "content": "Test Content"} response = test_client.post("/plans", json=request_data) assert response.status_code == 201 plan_id = response.json()["id"] From 54c104701f986d689c4ee03781e808e64c38246f Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Tue, 27 May 2025 17:35:51 +0200 Subject: [PATCH 20/30] bookmark_plan returns none, add TestBadDB test --- service/app/services/plan_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 7204b26..4ee4556 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -111,4 +111,3 @@ def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: session.add(plan) session.commit() session.refresh(plan) - From db562ea13f853cd2f674654c3d8b77c15dc9896b Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 22:50:25 +0200 Subject: [PATCH 21/30] run linter --- service/app/create_tables.py | 4 ---- service/app/services/plan_service.py | 5 ----- 2 files changed, 9 deletions(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index e188b80..ae9e996 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -3,10 +3,6 @@ # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 -# Import models so their metadata is registered with SQLModel -from models import plan, user # noqa: F401 - - def main() -> None: create_db_and_tables() print("Database tables created!") diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 4ee4556..bce25b5 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -106,8 +106,3 @@ def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: if plan.user_id != user_id: error_msg = "Plan not found or access denied" raise ValueError(error_msg) - - plan.bookmark = not plan.bookmark - session.add(plan) - session.commit() - session.refresh(plan) From 01cf704ebcb13efae3435977f46c4ede127ce7a4 Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sat, 17 May 2025 17:45:08 +0200 Subject: [PATCH 22/30] update tests --- service/tests/test_plan_router.py | 1 + 1 file changed, 1 insertion(+) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 73a7b9c..cdb6c67 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,3 +1,4 @@ +from collections.abc import Iterator import uuid import pytest from fastapi.testclient import TestClient From ecdb9d11c689fac3756512a7107b1e93f273448d Mon Sep 17 00:00:00 2001 From: pheonix8 <61965711+pheonix8@users.noreply.github.com> Date: Sat, 17 May 2025 17:56:04 +0200 Subject: [PATCH 23/30] fix linting errors --- service/tests/test_plan_router.py | 1 - 1 file changed, 1 deletion(-) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index cdb6c67..73a7b9c 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,4 +1,3 @@ -from collections.abc import Iterator import uuid import pytest from fastapi.testclient import TestClient From 25ece443917cead2fef6a77a3ae82eac4949cffe Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 19 May 2025 22:50:25 +0200 Subject: [PATCH 24/30] rebase main into feature/bookmark-plans --- service/app/create_tables.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index ae9e996..390ff89 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,7 +1,11 @@ from database import create_db_and_tables +<<<<<<< HEAD # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 +======= +from models import plan, user +>>>>>>> 504c450 (run linter) def main() -> None: create_db_and_tables() From 5dd85634caaa47524d7b8a4ebfc5858e7c597c7c Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 20:30:39 +0200 Subject: [PATCH 25/30] fixed merge conflicts --- service/app/create_tables.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index 390ff89..ae9e996 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -1,11 +1,7 @@ from database import create_db_and_tables -<<<<<<< HEAD # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 -======= -from models import plan, user ->>>>>>> 504c450 (run linter) def main() -> None: create_db_and_tables() From 0b46097854643f573f0ce5807aa1c636d69d3a88 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Mon, 26 May 2025 20:59:58 +0200 Subject: [PATCH 26/30] changed router.patch to return 204 --- service/app/routers/plan_router.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index c15d014..5075be2 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -73,7 +73,10 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e +<<<<<<< HEAD +======= +>>>>>>> a7917ce (changed router.patch to return 204) @router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: From 9154b7a814804d2875c35262cd7a2ff88c0939cf Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Sun, 1 Jun 2025 14:36:14 +0200 Subject: [PATCH 27/30] error 401 --- service/app/routers/plan_router.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 5075be2..87b7b26 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -73,10 +73,6 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e -<<<<<<< HEAD - -======= ->>>>>>> a7917ce (changed router.patch to return 204) @router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: From 56cacf27bd70c4d2e6aa1ee1261af69345526ed7 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Sun, 1 Jun 2025 15:27:31 +0200 Subject: [PATCH 28/30] correct bookmark plan --- service/app/services/plan_service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index bce25b5..4ee4556 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -106,3 +106,8 @@ def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: if plan.user_id != user_id: error_msg = "Plan not found or access denied" raise ValueError(error_msg) + + plan.bookmark = not plan.bookmark + session.add(plan) + session.commit() + session.refresh(plan) From 01bb3ce79030fc98c8c3fe64c18acf44ebb9c465 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Sun, 1 Jun 2025 15:47:47 +0200 Subject: [PATCH 29/30] ran linter and pytests --- service/app/create_tables.py | 1 + service/app/database.py | 2 -- service/app/models/plan.py | 3 +-- service/app/services/plan_service.py | 7 +++---- service/tests/test_plan_router.py | 11 ++--------- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/service/app/create_tables.py b/service/app/create_tables.py index ae9e996..c0b34cc 100644 --- a/service/app/create_tables.py +++ b/service/app/create_tables.py @@ -3,6 +3,7 @@ # Import models so their metadata is registered with SQLModel from models import plan, user # noqa: F401 + def main() -> None: create_db_and_tables() print("Database tables created!") diff --git a/service/app/database.py b/service/app/database.py index aa79120..cd541f9 100644 --- a/service/app/database.py +++ b/service/app/database.py @@ -5,8 +5,6 @@ from dotenv import load_dotenv from sqlalchemy.orm import sessionmaker from sqlmodel import Session, SQLModel, create_engine -from sqlalchemy.orm import sessionmaker -from sqlmodel import Session, SQLModel, create_engine load_dotenv() diff --git a/service/app/models/plan.py b/service/app/models/plan.py index 21083a5..c3260fb 100644 --- a/service/app/models/plan.py +++ b/service/app/models/plan.py @@ -1,6 +1,5 @@ import uuid from datetime import datetime -from typing import Optional from sqlmodel import Field, SQLModel @@ -10,7 +9,7 @@ class Plan(SQLModel, table=True): group_version_id: uuid.UUID = Field(default_factory=uuid.uuid4) name: str = Field(nullable=False) content: str - public_slug: Optional[str] + public_slug: str | None bookmark: bool = Field(default=False) created_at: datetime = Field(default_factory=datetime.now) user_id: uuid.UUID = Field(nullable=False, foreign_key="user.id") diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index 4ee4556..c2dbb72 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -2,8 +2,8 @@ from collections.abc import Sequence from uuid import UUID +from sqlalchemy import and_, func from sqlmodel import Session, select -from sqlalchemy import func, and_ from ..models.plan import Plan from ..schemas.plan import PlanCreate, PlanRead, PlanUpdate @@ -45,7 +45,7 @@ def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: return PlanRead.model_validate(plan) -def get_plan_history(user_id: UUID, plan_id: UUID, session: Session) -> Sequence[PlanRead]: +def get_plan_history(plan_id: UUID, session: Session) -> Sequence[PlanRead]: current_plan = session.get(Plan, plan_id) statement = ( select(Plan) @@ -79,8 +79,7 @@ def delete_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: session.commit() def create_public_slug(group_id: UUID) -> str: - slug = base64.urlsafe_b64encode(group_id.bytes).rstrip(b'=').decode('ascii') - return slug + return base64.urlsafe_b64encode(group_id.bytes).rstrip(b"=").decode("ascii") def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Session) -> PlanRead: current_plan = session.get(Plan, plan_id) diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index 73a7b9c..b929110 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -1,4 +1,5 @@ import uuid + import pytest from fastapi.testclient import TestClient @@ -112,9 +113,6 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/history/{uuid.uuid4()}") assert response.status_code == 500 - def test_create_plan(self, test_client: TestClient) -> None: - request_data = {"name": "Test Plan", "content": "Test Content"} - class TestPublicSlug: def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: request_data = {"name": "Test Plan", "content": "Test Content"} @@ -130,7 +128,7 @@ def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: assert data["content"] == request_data["content"] def test_get_nonexisting_plan(self, test_client: TestClient) -> None: - response = test_client.get(f"/plans/shared/testSlug") + response = test_client.get("/plans/shared/testSlug") assert response.status_code == 404 @@ -162,8 +160,3 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.post(f"/plans/{plan_id}/update", json=request_data) assert response.status_code == 201 plan_id = response.json()["id"] - - response = test_client.get(f"/plans/history/{plan_id}") - assert response.status_code == 200 - data = response.json() - assert len(data["plans"]) == 2 From cb7cd7c02f3150a7b7f0901a3771bf0d38c4aca0 Mon Sep 17 00:00:00 2001 From: Roman Weber Date: Sun, 1 Jun 2025 15:53:53 +0200 Subject: [PATCH 30/30] uv run ruff format --- service/app/routers/plan_router.py | 9 +++++++-- service/app/schemas/plan.py | 1 + service/app/services/plan_service.py | 29 +++++++++++----------------- service/tests/conftest.py | 6 +----- service/tests/test_plan_router.py | 7 ++++--- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/service/app/routers/plan_router.py b/service/app/routers/plan_router.py index 87b7b26..4fa6ec2 100644 --- a/service/app/routers/plan_router.py +++ b/service/app/routers/plan_router.py @@ -34,9 +34,10 @@ async def get_plan_by_public_slug(public_slug: str, session: Annotated[Session, raise HTTPException(status_code=500, detail="Failed to get plan. Please report to page-admin") from e return plan + @router.get("/plans/history/{plan_id}", dependencies=[Depends(auth_dependency)]) -async def get_plan_history( - request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)] +async def get_plan_history( + request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)] ) -> dict[str, Sequence[PlanRead]]: try: plans = plan_service.get_plan_history(request.state.user.id, plan_id, session) @@ -44,6 +45,7 @@ async def get_plan_history( raise HTTPException(status_code=500, detail="Failed to get plans. Please report to page-admin") from e return {"plans": plans} + @router.post("/plans", dependencies=[Depends(auth_dependency)], status_code=201) async def create_plan( request: Request, plan_data: PlanCreate, session: Annotated[Session, Depends(get_session)] @@ -54,6 +56,7 @@ async def create_plan( raise HTTPException(status_code=500, detail="Failed to create plan. Please report to page-admin") from e return created_plan + @router.post("/plans/{plan_id}/update", dependencies=[Depends(auth_dependency)], status_code=201) async def update_plan( request: Request, plan_id: UUID, plan_data: PlanUpdate, session: Annotated[Session, Depends(get_session)] @@ -64,6 +67,7 @@ async def update_plan( raise HTTPException(status_code=500, detail="Failed to create plan. Please report to page-admin") from e return created_plan + @router.delete("/plans/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: @@ -73,6 +77,7 @@ async def delete_plan(request: Request, plan_id: UUID, session: Annotated[Sessio except Exception as e: raise HTTPException(status_code=500, detail="Failed to delete plan. Please report to page-admin") from e + @router.patch("/plans/bookmark/{plan_id}", dependencies=[Depends(auth_dependency)], status_code=204) async def bookmark_plan(request: Request, plan_id: UUID, session: Annotated[Session, Depends(get_session)]) -> None: try: diff --git a/service/app/schemas/plan.py b/service/app/schemas/plan.py index a57c86c..02931e5 100644 --- a/service/app/schemas/plan.py +++ b/service/app/schemas/plan.py @@ -19,5 +19,6 @@ class PlanCreate(SQLModel): name: str content: str + class PlanUpdate(SQLModel): content: str diff --git a/service/app/services/plan_service.py b/service/app/services/plan_service.py index c2dbb72..d02f1a0 100644 --- a/service/app/services/plan_service.py +++ b/service/app/services/plan_service.py @@ -11,21 +11,17 @@ def get_plans(user_id: UUID, session: Session) -> Sequence[PlanRead]: subquery = ( - select( - Plan.group_version_id, - func.max(Plan.created_at).label("max_created_at") - ) + select(Plan.group_version_id, func.max(Plan.created_at).label("max_created_at")) .where(Plan.user_id == user_id) .group_by(Plan.group_version_id) .subquery() ) statement = ( select(Plan) - .join(subquery, - and_( - Plan.group_version_id == subquery.c.group_version_id, - Plan.created_at == subquery.c.max_created_at - )) + .join( + subquery, + and_(Plan.group_version_id == subquery.c.group_version_id, Plan.created_at == subquery.c.max_created_at), + ) .where(Plan.user_id == user_id) ) plans = session.exec(statement).all() @@ -33,11 +29,7 @@ def get_plans(user_id: UUID, session: Session) -> Sequence[PlanRead]: def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: - statement = ( - select(Plan) - .where(Plan.public_slug == public_slug) - .order_by(Plan.created_at.desc()) - ) + statement = select(Plan).where(Plan.public_slug == public_slug).order_by(Plan.created_at.desc()) plan = session.exec(statement).first() if not plan: error_msg = "Plan not found" @@ -48,9 +40,7 @@ def get_plan_by_public_slug(public_slug: str, session: Session) -> PlanRead: def get_plan_history(plan_id: UUID, session: Session) -> Sequence[PlanRead]: current_plan = session.get(Plan, plan_id) statement = ( - select(Plan) - .where(Plan.group_version_id == current_plan.group_version_id) - .order_by(Plan.created_at.desc()) + select(Plan).where(Plan.group_version_id == current_plan.group_version_id).order_by(Plan.created_at.desc()) ) plans = session.exec(statement).all() return [PlanRead.model_validate(plan) for plan in plans] @@ -78,9 +68,11 @@ def delete_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: session.delete(plan) session.commit() + def create_public_slug(group_id: UUID) -> str: return base64.urlsafe_b64encode(group_id.bytes).rstrip(b"=").decode("ascii") + def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Session) -> PlanRead: current_plan = session.get(Plan, plan_id) new_plan_data = { @@ -89,7 +81,7 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se "content": plan_data.content, "public_slug": current_plan.public_slug, "bookmark": current_plan.bookmark, - "user_id": user_id + "user_id": user_id, } new_plan = Plan(**new_plan_data) session.add(new_plan) @@ -97,6 +89,7 @@ def update_plan(user_id: UUID, plan_id: UUID, plan_data: PlanUpdate, session: Se session.refresh(new_plan) return PlanRead.model_validate(new_plan) + def bookmark_plan(user_id: UUID, plan_id: UUID, session: Session) -> None: plan = session.get(Plan, plan_id) if not plan: diff --git a/service/tests/conftest.py b/service/tests/conftest.py index c1e92e1..fe5796e 100644 --- a/service/tests/conftest.py +++ b/service/tests/conftest.py @@ -29,11 +29,7 @@ async def override_auth_dependency(request: Request, session: Annotated[Session, def override_get_session() -> Generator[Session, Any]: - engine = create_engine( - "sqlite://", - connect_args={"check_same_thread": False}, - poolclass=StaticPool - ) + engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool) session = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=Session) db = session() diff --git a/service/tests/test_plan_router.py b/service/tests/test_plan_router.py index b929110..0dad734 100644 --- a/service/tests/test_plan_router.py +++ b/service/tests/test_plan_router.py @@ -113,6 +113,7 @@ def test_get_plan_history(self, test_client: TestClient) -> None: response = test_client.get(f"/plans/history/{uuid.uuid4()}") assert response.status_code == 500 + class TestPublicSlug: def test_get_plan_by_public_slug(self, test_client: TestClient) -> None: request_data = {"name": "Test Plan", "content": "Test Content"} @@ -139,13 +140,13 @@ def test_update_plan(self, test_client: TestClient) -> None: assert response.status_code == 201 response = test_client.get("/plans") - assert response.status_code == 200 + assert response.status_code == 200 data = response.json() assert len(data["plans"]) == 1 plan_id = data["plans"][0]["id"] request_data = {"content": "New Test Content"} - response = test_client.post(f"/plans/{plan_id}/update", json=request_data) + response = test_client.post(f"/plans/{plan_id}/update", json=request_data) assert response.status_code == 201 data = response.json() assert data["content"] == request_data["content"] @@ -157,6 +158,6 @@ def test_get_plan_history(self, test_client: TestClient) -> None: plan_id = response.json()["id"] request_data = {"content": "New Test Content"} - response = test_client.post(f"/plans/{plan_id}/update", json=request_data) + response = test_client.post(f"/plans/{plan_id}/update", json=request_data) assert response.status_code == 201 plan_id = response.json()["id"]