From dbb8171e86ef6846db88222136cb342f0b617fe2 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 1 Mar 2024 03:19:10 +0100 Subject: [PATCH 01/21] add recommendation module --- app/modules/recommendation/__init__.py | 0 .../recommendation/cruds_recommendation.py | 65 +++++++++++ .../endpoints_recommendation.py | 104 ++++++++++++++++++ .../recommendation/models_recommendation.py | 14 +++ .../recommendation/schemas_recommendation.py | 22 ++++ tests/test_recommendation.py | 81 ++++++++++++++ 6 files changed, 286 insertions(+) create mode 100644 app/modules/recommendation/__init__.py create mode 100644 app/modules/recommendation/cruds_recommendation.py create mode 100644 app/modules/recommendation/endpoints_recommendation.py create mode 100644 app/modules/recommendation/models_recommendation.py create mode 100644 app/modules/recommendation/schemas_recommendation.py create mode 100644 tests/test_recommendation.py diff --git a/app/modules/recommendation/__init__.py b/app/modules/recommendation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py new file mode 100644 index 000000000..0c8b881ea --- /dev/null +++ b/app/modules/recommendation/cruds_recommendation.py @@ -0,0 +1,65 @@ +import uuid +from typing import Sequence + +from sqlalchemy import delete, select, update +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.recommendation import models_recommendation, schemas_recommendation + + +async def get_recommendation( + db: AsyncSession, +) -> Sequence[models_recommendation.Recommendation]: + result = await db.execute(select(models_recommendation.Recommendation)) + return result.scalars().all() + + +async def create_recommendation( + recommendation: schemas_recommendation.RecommendationBase, + db: AsyncSession, +) -> models_recommendation.Recommendation: + recommendation_db = models_recommendation.Recommendation( + id=str(uuid.uuid4()), + **recommendation.dict(), + ) + db.add(recommendation_db) + try: + await db.commit() + except IntegrityError as error: + await db.rollback() + raise ValueError(error) + return recommendation_db + + +async def update_recommendation( + recommendation_id: str, + recommendation: schemas_recommendation.RecommendationEdit, + db: AsyncSession, +): + await db.execute( + update(models_recommendation.Recommendation) + .where(models_recommendation.Recommendation.id == recommendation_id) + .values(**recommendation.dict(exclude_none=True)) + ) + try: + await db.commit() + except IntegrityError: + await db.rollback() + raise ValueError() + + +async def delete_recommendation( + recommendation_id: str, + db: AsyncSession, +): + await db.execute( + delete(models_recommendation.Recommendation).where( + models_recommendation.Recommendation.id == recommendation_id + ) + ) + try: + await db.commit() + except IntegrityError as error: + await db.rollback() + raise ValueError(error) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py new file mode 100644 index 000000000..804551690 --- /dev/null +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -0,0 +1,104 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core import models_core +from app.core.groups.groups_type import GroupType +from app.core.module import Module +from app.dependencies import get_db, is_user_a_member, is_user_a_member_of +from app.modules.recommendation import cruds_recommendation, schemas_recommendation + +router = APIRouter() + + +module = Module( + root="recommendation", + tag="Recommendation", + default_allowed_groups_ids=[GroupType.student, GroupType.staff], +) + + +@module.router.get( + "/recommendation/recommendations", + response_model=list[schemas_recommendation.Recommendation], + status_code=200, +) +async def get_recommendation( + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), +): + """ + Get recommendations. + + **The user must be authenticated to use this endpoint** + """ + + return await cruds_recommendation.get_recommendation(db=db) + + +@module.router.post( + "/recommendation/recommendations", + response_model=schemas_recommendation.Recommendation, + status_code=201, +) +async def create_recommendation( + recommendation: schemas_recommendation.RecommendationBase, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDE)), +): + """ + Create a recommendation. + + **This endpoint is only usable by members of the group BDE** + """ + + try: + return await cruds_recommendation.create_recommendation( + recommendation=recommendation, + db=db, + ) + except ValueError as error: + raise HTTPException(status_code=422, detail=str(error)) + + +@module.router.patch( + "/recommendation/recommendations/{recommendation_id}", + status_code=204, +) +async def edit_recommendation( + recommendation_id: str, + recommendation: schemas_recommendation.RecommendationEdit, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDE)), +): + """ + Edit a recommendation. + + **This endpoint is only usable by members of the group BDE** + """ + + await cruds_recommendation.update_recommendation( + recommendation_id=recommendation_id, + recommendation=recommendation, + db=db, + ) + + +@module.router.delete( + "/recommendation/recommendations/{recommendation_id}", + status_code=204, +) +async def delete_recommendation( + recommendation_id: str, + db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDE)), +): + """ + Delete a recommendation. + + **This endpoint is only usable by members of the group BDE** + """ + + await cruds_recommendation.delete_recommendation( + db=db, + recommendation_id=recommendation_id, + ) diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py new file mode 100644 index 000000000..2370dfa0c --- /dev/null +++ b/app/modules/recommendation/models_recommendation.py @@ -0,0 +1,14 @@ +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from app.database import Base + + +class Recommendation(Base): + __tablename__ = "recommendation" + + id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) + title: Mapped[str] = mapped_column(String, nullable=False) + code: Mapped[str] = mapped_column(String, nullable=False) + summary: Mapped[str] = mapped_column(String, nullable=False) + description: Mapped[str] = mapped_column(String, nullable=False) diff --git a/app/modules/recommendation/schemas_recommendation.py b/app/modules/recommendation/schemas_recommendation.py new file mode 100644 index 000000000..80a8b10f1 --- /dev/null +++ b/app/modules/recommendation/schemas_recommendation.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel + + +class RecommendationBase(BaseModel): + title: str + code: str + summary: str + description: str + + +class Recommendation(RecommendationBase): + id: str + + class Config: + orm_mode = True + + +class RecommendationEdit(BaseModel): + title: str | None = None + code: str | None = None + summary: str | None = None + description: str | None = None diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py new file mode 100644 index 000000000..605e1e8b7 --- /dev/null +++ b/tests/test_recommendation.py @@ -0,0 +1,81 @@ +import uuid + +import pytest_asyncio + +from app.core.groups.groups_type import GroupType +from app.modules.recommendation import models_recommendation +from tests.commons import event_loop # noqa +from tests.commons import ( + add_object_to_db, + client, + create_api_access_token, + create_user_with_groups, +) + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def init_objects(): + user_simple = await create_user_with_groups([GroupType.student]) + + global token_simple + token_simple = create_api_access_token(user_simple) + + user_BDE = await create_user_with_groups([GroupType.BDE]) + + global token_BDE + token_BDE = create_api_access_token(user_BDE) + + global recommendation + recommendation = models_recommendation.Recommendation( + id=str(uuid.uuid4()), + title="Un titre", + code="Un code", + summary="Un résumé", + description="Une description", + ) + await add_object_to_db(recommendation) + + +def test_get_recommendation(): + response = client.get( + "/recommendation/recommendations", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + assert response.status_code == 200 + + +def test_create_recommendation(): + response = client.post( + "/recommendation/recommendations", + json={ + "title": "Un titre", + "code": "Un code", + "summary": "Un résumé", + "description": "Une description", + }, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 201 + response = client.post( + "/recommendation/recommendations", + json={}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 422 + + +def test_edit_recommendation(): + response = client.patch( + f"/recommendation/recommendations/{recommendation.id}", + json={"title": "Nouveau titre"}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 204 + + +def test_delete_recommendation(): + response = client.delete( + f"/recommendation/recommendations/{recommendation.id}", + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 204 From 92fb55385b6d88988e9988b8c0849f750bcd02b4 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:12:53 +0100 Subject: [PATCH 02/21] add image upload and download to recommendation --- .../recommendation/cruds_recommendation.py | 12 +++ .../endpoints_recommendation.py | 73 +++++++++++++++++- assets/images/default_recommendation.png | Bin 0 -> 266 bytes tests/test_recommendation.py | 20 +++++ 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 assets/images/default_recommendation.png diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 0c8b881ea..f979adebb 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -63,3 +63,15 @@ async def delete_recommendation( except IntegrityError as error: await db.rollback() raise ValueError(error) + + +async def get_recommendation_by_id( + recommendation_id: str, + db: AsyncSession, +) -> models_recommendation.Recommendation: + result = await db.execute( + select(models_recommendation.Recommendation).where( + models_recommendation.Recommendation.id == recommendation_id + ) + ) + return result.scalars().one() diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 804551690..8fb6e869a 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -1,11 +1,18 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile +from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession -from app.core import models_core +from app.core import models_core, standard_responses from app.core.groups.groups_type import GroupType from app.core.module import Module -from app.dependencies import get_db, is_user_a_member, is_user_a_member_of +from app.dependencies import ( + get_db, + get_request_id, + is_user_a_member, + is_user_a_member_of, +) from app.modules.recommendation import cruds_recommendation, schemas_recommendation +from app.utils.tools import get_file_from_data, save_file_as_data router = APIRouter() @@ -102,3 +109,63 @@ async def delete_recommendation( db=db, recommendation_id=recommendation_id, ) + + +@module.router.get( + "/recommendation/recommendations/{recommendation_id}/picture", + response_class=FileResponse, + status_code=200, +) +async def read_recommendation_image( + recommendation_id: str, +): + """ + Get the image of a recommendation. + + **The user must be authenticated to use this endpoint** + """ + return get_file_from_data( + default_asset="assets/images/default_recommendation.png", + directory="recommendations", + filename=str(recommendation_id), + ) + + +@module.router.post( + "/recommendation/recommendations/{recommendation_id}/picture", + response_model=standard_responses.Result, + status_code=201, +) +async def create_recommendation_image( + recommendation_id: str, + image: UploadFile = File(), + user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDE)), + request_id: str = Depends(get_request_id), + db: AsyncSession = Depends(get_db), +): + """ + Add an image to a recommendation. + + **This endpoint is only usable by members of the group BDE** + """ + recommendation = await cruds_recommendation.get_recommendation_by_id( + recommendation_id=recommendation_id, + db=db, + ) + + if recommendation is None: + raise HTTPException( + status_code=404, + detail="The recommendation does not exist", + ) + + await save_file_as_data( + image=image, + directory="recommendations", + filename=str(recommendation_id), + request_id=request_id, + max_file_size=4 * 1024 * 1024, + accepted_content_types=["image/jpeg", "image/png", "image/webp"], + ) + + return standard_responses.Result(success=True) diff --git a/assets/images/default_recommendation.png b/assets/images/default_recommendation.png new file mode 100644 index 0000000000000000000000000000000000000000..0e84866445983977e9c2cdbb3475c93b8089f766 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*@xBpA$Gw#oo0mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5yxS$b1ju7A z@$_|Nf6gN%sIT?1>G2$(kdUW~V~E7%=JG-9UYk&zLx}~WX)T47$~b+;u=ws zl30>zm0Xkxq!^40jEr>+jdYF7LJZBVOn}Hp+rYrez<|kYz5inG;0 P_A_|8`njxgN@xNA1ExX8 literal 0 HcmV?d00001 diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py index 605e1e8b7..e65341b2c 100644 --- a/tests/test_recommendation.py +++ b/tests/test_recommendation.py @@ -36,6 +36,26 @@ async def init_objects(): await add_object_to_db(recommendation) +def test_create_picture(): + with open("assets/images/default_recommendation.png", "rb") as image: + response = client.post( + f"/recommendation/recommendations/{recommendation.id}/picture", + files={"image": ("recommendation.png", image, "image/png")}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + + assert response.status_code == 201 + + +def test_get_picture(): + response = client.get( + f"/recommendation/recommendations/{recommendation.id}/picture", + headers={"Authorization": f"Bearer {token_simple}"}, + ) + + assert response.status_code == 200 + + def test_get_recommendation(): response = client.get( "/recommendation/recommendations", From 0b6f16563ed50a64adc9345b33ab95ee72c5d255 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 1 Mar 2024 03:25:00 +0100 Subject: [PATCH 03/21] add a creation datetime to recommendation --- app/modules/recommendation/cruds_recommendation.py | 5 +++++ app/modules/recommendation/endpoints_recommendation.py | 4 ++++ app/modules/recommendation/models_recommendation.py | 1 + app/modules/recommendation/schemas_recommendation.py | 3 +++ 4 files changed, 13 insertions(+) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index f979adebb..b5c2126bf 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -1,10 +1,13 @@ import uuid +from datetime import datetime from typing import Sequence +from zoneinfo import ZoneInfo from sqlalchemy import delete, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession +from app.core.config import Settings from app.modules.recommendation import models_recommendation, schemas_recommendation @@ -18,9 +21,11 @@ async def get_recommendation( async def create_recommendation( recommendation: schemas_recommendation.RecommendationBase, db: AsyncSession, + settings: Settings, ) -> models_recommendation.Recommendation: recommendation_db = models_recommendation.Recommendation( id=str(uuid.uuid4()), + creation=datetime.now(ZoneInfo(settings.TIMEZONE)), **recommendation.dict(), ) db.add(recommendation_db) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 8fb6e869a..67e93e820 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -3,11 +3,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core import models_core, standard_responses +from app.core.config import Settings from app.core.groups.groups_type import GroupType from app.core.module import Module from app.dependencies import ( get_db, get_request_id, + get_settings, is_user_a_member, is_user_a_member_of, ) @@ -50,6 +52,7 @@ async def get_recommendation( async def create_recommendation( recommendation: schemas_recommendation.RecommendationBase, db: AsyncSession = Depends(get_db), + settings: Settings = Depends(get_settings), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.BDE)), ): """ @@ -62,6 +65,7 @@ async def create_recommendation( return await cruds_recommendation.create_recommendation( recommendation=recommendation, db=db, + settings=settings, ) except ValueError as error: raise HTTPException(status_code=422, detail=str(error)) diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py index 2370dfa0c..166eeaf14 100644 --- a/app/modules/recommendation/models_recommendation.py +++ b/app/modules/recommendation/models_recommendation.py @@ -8,6 +8,7 @@ class Recommendation(Base): __tablename__ = "recommendation" id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) + creation: Mapped[str] = mapped_column(String, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) code: Mapped[str] = mapped_column(String, nullable=False) summary: Mapped[str] = mapped_column(String, nullable=False) diff --git a/app/modules/recommendation/schemas_recommendation.py b/app/modules/recommendation/schemas_recommendation.py index 80a8b10f1..216463836 100644 --- a/app/modules/recommendation/schemas_recommendation.py +++ b/app/modules/recommendation/schemas_recommendation.py @@ -1,3 +1,5 @@ +from datetime import datetime + from pydantic import BaseModel @@ -10,6 +12,7 @@ class RecommendationBase(BaseModel): class Recommendation(RecommendationBase): id: str + creation: datetime class Config: orm_mode = True From 7472b5693a8d54ce18b00d9c65de4c8c64551cf7 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:16:25 +0100 Subject: [PATCH 04/21] fix creation type --- app/modules/recommendation/models_recommendation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py index 166eeaf14..373fdace0 100644 --- a/app/modules/recommendation/models_recommendation.py +++ b/app/modules/recommendation/models_recommendation.py @@ -1,4 +1,6 @@ -from sqlalchemy import String +from datetime import datetime + +from sqlalchemy import DateTime, String from sqlalchemy.orm import Mapped, mapped_column from app.database import Base @@ -8,7 +10,7 @@ class Recommendation(Base): __tablename__ = "recommendation" id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) - creation: Mapped[str] = mapped_column(String, nullable=False) + creation: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) code: Mapped[str] = mapped_column(String, nullable=False) summary: Mapped[str] = mapped_column(String, nullable=False) From 33293a10c8da18030d23ee2798f6abf692ff20d8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 26 Dec 2023 19:16:49 +0100 Subject: [PATCH 05/21] fix test --- tests/test_recommendation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py index e65341b2c..a48e0d923 100644 --- a/tests/test_recommendation.py +++ b/tests/test_recommendation.py @@ -1,4 +1,5 @@ import uuid +from datetime import datetime import pytest_asyncio @@ -28,6 +29,7 @@ async def init_objects(): global recommendation recommendation = models_recommendation.Recommendation( id=str(uuid.uuid4()), + creation=datetime.now(), title="Un titre", code="Un code", summary="Un résumé", From f5518a250afaaf2242e2e803c6901a6003e7a5eb Mon Sep 17 00:00:00 2001 From: julien4215 <120588494+julien4215@users.noreply.github.com> Date: Sun, 14 Jan 2024 18:56:05 +0100 Subject: [PATCH 06/21] Change status code Co-authored-by: Armand Didierjean <95971503+armanddidierjean@users.noreply.github.com> --- app/modules/recommendation/endpoints_recommendation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 67e93e820..60d5d58c5 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -68,7 +68,7 @@ async def create_recommendation( settings=settings, ) except ValueError as error: - raise HTTPException(status_code=422, detail=str(error)) + raise HTTPException(status_code=400, detail=str(error)) @module.router.patch( From 50a5d9f7a826e0a54a9c525293a0ff5904177bd8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:51:12 +0100 Subject: [PATCH 07/21] improve HTTP errors --- .../recommendation/cruds_recommendation.py | 29 +++++----- .../endpoints_recommendation.py | 55 ++++++++++--------- tests/test_recommendation.py | 26 ++++++++- 3 files changed, 67 insertions(+), 43 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index b5c2126bf..81ecd236e 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -4,7 +4,7 @@ from zoneinfo import ZoneInfo from sqlalchemy import delete, select, update -from sqlalchemy.exc import IntegrityError +from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings @@ -29,11 +29,7 @@ async def create_recommendation( **recommendation.dict(), ) db.add(recommendation_db) - try: - await db.commit() - except IntegrityError as error: - await db.rollback() - raise ValueError(error) + await db.commit() return recommendation_db @@ -42,32 +38,32 @@ async def update_recommendation( recommendation: schemas_recommendation.RecommendationEdit, db: AsyncSession, ): - await db.execute( + result = await db.execute( update(models_recommendation.Recommendation) .where(models_recommendation.Recommendation.id == recommendation_id) .values(**recommendation.dict(exclude_none=True)) ) - try: + if result.rowcount == 1: await db.commit() - except IntegrityError: + else: await db.rollback() - raise ValueError() + raise ValueError async def delete_recommendation( recommendation_id: str, db: AsyncSession, ): - await db.execute( + result = await db.execute( delete(models_recommendation.Recommendation).where( models_recommendation.Recommendation.id == recommendation_id ) ) - try: + if result.rowcount == 1: await db.commit() - except IntegrityError as error: + else: await db.rollback() - raise ValueError(error) + raise ValueError async def get_recommendation_by_id( @@ -79,4 +75,7 @@ async def get_recommendation_by_id( models_recommendation.Recommendation.id == recommendation_id ) ) - return result.scalars().one() + try: + return result.scalars().one() + except NoResultFound: + raise ValueError diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 60d5d58c5..3219ca6fd 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -61,14 +61,11 @@ async def create_recommendation( **This endpoint is only usable by members of the group BDE** """ - try: - return await cruds_recommendation.create_recommendation( - recommendation=recommendation, - db=db, - settings=settings, - ) - except ValueError as error: - raise HTTPException(status_code=400, detail=str(error)) + return await cruds_recommendation.create_recommendation( + recommendation=recommendation, + db=db, + settings=settings, + ) @module.router.patch( @@ -87,11 +84,17 @@ async def edit_recommendation( **This endpoint is only usable by members of the group BDE** """ - await cruds_recommendation.update_recommendation( - recommendation_id=recommendation_id, - recommendation=recommendation, - db=db, - ) + if any(recommendation.dict().values()): + try: + await cruds_recommendation.update_recommendation( + recommendation_id=recommendation_id, + recommendation=recommendation, + db=db, + ) + except ValueError: + raise HTTPException( + status_code=404, detail="The recommendation does not exist" + ) @module.router.delete( @@ -109,10 +112,13 @@ async def delete_recommendation( **This endpoint is only usable by members of the group BDE** """ - await cruds_recommendation.delete_recommendation( - db=db, - recommendation_id=recommendation_id, - ) + try: + await cruds_recommendation.delete_recommendation( + db=db, + recommendation_id=recommendation_id, + ) + except ValueError: + raise HTTPException(status_code=404, detail="The recommendation does not exist") @module.router.get( @@ -152,16 +158,13 @@ async def create_recommendation_image( **This endpoint is only usable by members of the group BDE** """ - recommendation = await cruds_recommendation.get_recommendation_by_id( - recommendation_id=recommendation_id, - db=db, - ) - - if recommendation is None: - raise HTTPException( - status_code=404, - detail="The recommendation does not exist", + try: + await cruds_recommendation.get_recommendation_by_id( + recommendation_id=recommendation_id, + db=db, ) + except ValueError: + raise HTTPException(status_code=404, detail="The recommendation does not exist") await save_file_as_data( image=image, diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py index a48e0d923..1f181b7b0 100644 --- a/tests/test_recommendation.py +++ b/tests/test_recommendation.py @@ -45,8 +45,14 @@ def test_create_picture(): files={"image": ("recommendation.png", image, "image/png")}, headers={"Authorization": f"Bearer {token_BDE}"}, ) - assert response.status_code == 201 + with open("assets/images/default_recommendation.png", "rb") as image: + response = client.post( + "/recommendation/recommendations/false_id/picture", + files={"image": ("recommendation.png", image, "image/png")}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 404 def test_get_picture(): @@ -54,7 +60,6 @@ def test_get_picture(): f"/recommendation/recommendations/{recommendation.id}/picture", headers={"Authorization": f"Bearer {token_simple}"}, ) - assert response.status_code == 200 @@ -93,6 +98,18 @@ def test_edit_recommendation(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 204 + response = client.patch( + f"/recommendation/recommendations/{recommendation.id}", + json={}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 204 + response = client.patch( + "/recommendation/recommendations/false_id", + json={"title": "Nouveau titre"}, + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 404 def test_delete_recommendation(): @@ -101,3 +118,8 @@ def test_delete_recommendation(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 204 + response = client.delete( + "/recommendation/recommendations/false_id", + headers={"Authorization": f"Bearer {token_BDE}"}, + ) + assert response.status_code == 404 From 25a3b6151835a2967a9b56233a2dcc3c50c7bbfc Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:39:57 +0100 Subject: [PATCH 08/21] allow code to be null --- .../recommendation/cruds_recommendation.py | 4 +- .../endpoints_recommendation.py | 2 +- .../recommendation/models_recommendation.py | 2 +- .../recommendation/schemas_recommendation.py | 7 ++-- migrations/versions/4-recommendation.py | 40 +++++++++++++++++++ 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 migrations/versions/4-recommendation.py diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 81ecd236e..a856c1a2f 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -26,7 +26,7 @@ async def create_recommendation( recommendation_db = models_recommendation.Recommendation( id=str(uuid.uuid4()), creation=datetime.now(ZoneInfo(settings.TIMEZONE)), - **recommendation.dict(), + **recommendation.model_dump(), ) db.add(recommendation_db) await db.commit() @@ -41,7 +41,7 @@ async def update_recommendation( result = await db.execute( update(models_recommendation.Recommendation) .where(models_recommendation.Recommendation.id == recommendation_id) - .values(**recommendation.dict(exclude_none=True)) + .values(**recommendation.model_dump(exclude_none=True)) ) if result.rowcount == 1: await db.commit() diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 3219ca6fd..2af79e585 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -84,7 +84,7 @@ async def edit_recommendation( **This endpoint is only usable by members of the group BDE** """ - if any(recommendation.dict().values()): + if any(recommendation.model_dump().values()): try: await cruds_recommendation.update_recommendation( recommendation_id=recommendation_id, diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py index 373fdace0..8d5152dc9 100644 --- a/app/modules/recommendation/models_recommendation.py +++ b/app/modules/recommendation/models_recommendation.py @@ -12,6 +12,6 @@ class Recommendation(Base): id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) creation: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) - code: Mapped[str] = mapped_column(String, nullable=False) + code: Mapped[str] = mapped_column(String, nullable=True) summary: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[str] = mapped_column(String, nullable=False) diff --git a/app/modules/recommendation/schemas_recommendation.py b/app/modules/recommendation/schemas_recommendation.py index 216463836..3485f5b57 100644 --- a/app/modules/recommendation/schemas_recommendation.py +++ b/app/modules/recommendation/schemas_recommendation.py @@ -1,11 +1,11 @@ from datetime import datetime -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class RecommendationBase(BaseModel): title: str - code: str + code: str | None = None summary: str description: str @@ -14,8 +14,7 @@ class Recommendation(RecommendationBase): id: str creation: datetime - class Config: - orm_mode = True + model_config = ConfigDict(from_attributes=True) class RecommendationEdit(BaseModel): diff --git a/migrations/versions/4-recommendation.py b/migrations/versions/4-recommendation.py new file mode 100644 index 000000000..dee1d40a8 --- /dev/null +++ b/migrations/versions/4-recommendation.py @@ -0,0 +1,40 @@ +"""recommendation + +Revision ID: d233bd29c521 +Revises: f17e6182b0a9 +Create Date: 2024-02-23 19:12:25.372249 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "d233bd29c521" +down_revision: Union[str, None] = "f17e6182b0a9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "recommendation", + sa.Column("id", sa.String(), nullable=False), + sa.Column("creation", sa.DateTime(timezone=True), nullable=False), + sa.Column("title", sa.String(), nullable=False), + sa.Column("code", sa.String(), nullable=True), + sa.Column("summary", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("recommendation") + # ### end Alembic commands ### From cbd6a06e833cd19dd90cab9ce522f59ad5c05c0a Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 1 Mar 2024 03:26:19 +0100 Subject: [PATCH 09/21] move logic to endpoint --- .../recommendation/cruds_recommendation.py | 18 ++++------------- .../endpoints_recommendation.py | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index a856c1a2f..8abf240f8 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -1,13 +1,9 @@ -import uuid -from datetime import datetime from typing import Sequence -from zoneinfo import ZoneInfo from sqlalchemy import delete, select, update from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession -from app.core.config import Settings from app.modules.recommendation import models_recommendation, schemas_recommendation @@ -19,18 +15,12 @@ async def get_recommendation( async def create_recommendation( - recommendation: schemas_recommendation.RecommendationBase, + recommendation: models_recommendation.Recommendation, db: AsyncSession, - settings: Settings, -) -> models_recommendation.Recommendation: - recommendation_db = models_recommendation.Recommendation( - id=str(uuid.uuid4()), - creation=datetime.now(ZoneInfo(settings.TIMEZONE)), - **recommendation.model_dump(), - ) - db.add(recommendation_db) +): + db.add(recommendation) await db.commit() - return recommendation_db + return recommendation async def update_recommendation( diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 2af79e585..9aac90b33 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -1,3 +1,7 @@ +import uuid +from datetime import datetime +from zoneinfo import ZoneInfo + from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession @@ -13,7 +17,11 @@ is_user_a_member, is_user_a_member_of, ) -from app.modules.recommendation import cruds_recommendation, schemas_recommendation +from app.modules.recommendation import ( + cruds_recommendation, + models_recommendation, + schemas_recommendation, +) from app.utils.tools import get_file_from_data, save_file_as_data router = APIRouter() @@ -61,10 +69,14 @@ async def create_recommendation( **This endpoint is only usable by members of the group BDE** """ + recommendation_db = models_recommendation.Recommendation( + id=str(uuid.uuid4()), + creation=datetime.now(ZoneInfo(settings.TIMEZONE)), + **recommendation.model_dump(), + ) + return await cruds_recommendation.create_recommendation( - recommendation=recommendation, - db=db, - settings=settings, + recommendation=recommendation_db, db=db ) From fd80b5252cbf2b51290260f2dba6d9c7f0d39bc3 Mon Sep 17 00:00:00 2001 From: julien4215 <120588494+julien4215@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:32:01 +0100 Subject: [PATCH 10/21] remove useless str cast Co-authored-by: Armand Didierjean <95971503+armanddidierjean@users.noreply.github.com> --- app/modules/recommendation/endpoints_recommendation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 9aac90b33..468361baf 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -149,7 +149,7 @@ async def read_recommendation_image( return get_file_from_data( default_asset="assets/images/default_recommendation.png", directory="recommendations", - filename=str(recommendation_id), + filename=recommendation_id, ) From 8651e63477fb752900d32df401e0ecfea236e4e8 Mon Sep 17 00:00:00 2001 From: julien4215 <120588494+julien4215@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:34:53 +0100 Subject: [PATCH 11/21] correct type Co-authored-by: Armand Didierjean <95971503+armanddidierjean@users.noreply.github.com> --- app/modules/recommendation/models_recommendation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py index 8d5152dc9..f8c7e388f 100644 --- a/app/modules/recommendation/models_recommendation.py +++ b/app/modules/recommendation/models_recommendation.py @@ -12,6 +12,7 @@ class Recommendation(Base): id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) creation: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) - code: Mapped[str] = mapped_column(String, nullable=True) + code: Mapped[str | None] = mapped_column(String, nullable=True) + summary: Mapped[str] = mapped_column(String, nullable=False) description: Mapped[str] = mapped_column(String, nullable=False) From f1604616dd576de067c99902d64b6ba4cbf807fd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:29:51 +0100 Subject: [PATCH 12/21] rename crud --- app/modules/recommendation/cruds_recommendation.py | 2 +- app/modules/recommendation/endpoints_recommendation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 8abf240f8..0fe6ec907 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -7,7 +7,7 @@ from app.modules.recommendation import models_recommendation, schemas_recommendation -async def get_recommendation( +async def get_recommendations( db: AsyncSession, ) -> Sequence[models_recommendation.Recommendation]: result = await db.execute(select(models_recommendation.Recommendation)) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 468361baf..45c85b0cc 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -49,7 +49,7 @@ async def get_recommendation( **The user must be authenticated to use this endpoint** """ - return await cruds_recommendation.get_recommendation(db=db) + return await cruds_recommendation.get_recommendations(db=db) @module.router.post( From ff595ab44e8fcb490d743bad867f96763a99ebe5 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:42:49 +0100 Subject: [PATCH 13/21] one test for each API call --- tests/test_recommendation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py index 1f181b7b0..cf73d8677 100644 --- a/tests/test_recommendation.py +++ b/tests/test_recommendation.py @@ -46,6 +46,9 @@ def test_create_picture(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 201 + + +def test_create_picture_for_non_existing_recommendation(): with open("assets/images/default_recommendation.png", "rb") as image: response = client.post( "/recommendation/recommendations/false_id/picture", @@ -83,6 +86,9 @@ def test_create_recommendation(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 201 + + +def test_create_recommendation_with_no_body(): response = client.post( "/recommendation/recommendations", json={}, @@ -98,12 +104,18 @@ def test_edit_recommendation(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 204 + + +def test_edit_recommendation_with_no_body(): response = client.patch( f"/recommendation/recommendations/{recommendation.id}", json={}, headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 204 + + +def test_edit_for_non_existing_recommendation(): response = client.patch( "/recommendation/recommendations/false_id", json={"title": "Nouveau titre"}, @@ -118,6 +130,9 @@ def test_delete_recommendation(): headers={"Authorization": f"Bearer {token_BDE}"}, ) assert response.status_code == 204 + + +def test_delete_for_non_existing_recommendation(): response = client.delete( "/recommendation/recommendations/false_id", headers={"Authorization": f"Bearer {token_BDE}"}, From 50eb0e89611d338f01652ce261f71d80909c8c15 Mon Sep 17 00:00:00 2001 From: julien4215 <120588494+julien4215@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:12:08 +0100 Subject: [PATCH 14/21] Add type in crud create_recommendation Co-authored-by: Armand Didierjean <95971503+armanddidierjean@users.noreply.github.com> --- app/modules/recommendation/cruds_recommendation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 0fe6ec907..8066dff86 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -17,7 +17,7 @@ async def get_recommendations( async def create_recommendation( recommendation: models_recommendation.Recommendation, db: AsyncSession, -): +) -> models_recommendation.Recommendation: db.add(recommendation) await db.commit() return recommendation From c71681b017fa2044e3a6928628b51ade1ffca0ea Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:26:41 +0100 Subject: [PATCH 15/21] move update non empty condition to crud --- .../recommendation/cruds_recommendation.py | 3 +++ .../endpoints_recommendation.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 8066dff86..7819a2742 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -28,6 +28,9 @@ async def update_recommendation( recommendation: schemas_recommendation.RecommendationEdit, db: AsyncSession, ): + if not any(recommendation.model_dump().values()): + return + result = await db.execute( update(models_recommendation.Recommendation) .where(models_recommendation.Recommendation.id == recommendation_id) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 45c85b0cc..9ea9b3a33 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -96,17 +96,14 @@ async def edit_recommendation( **This endpoint is only usable by members of the group BDE** """ - if any(recommendation.model_dump().values()): - try: - await cruds_recommendation.update_recommendation( - recommendation_id=recommendation_id, - recommendation=recommendation, - db=db, - ) - except ValueError: - raise HTTPException( - status_code=404, detail="The recommendation does not exist" - ) + try: + await cruds_recommendation.update_recommendation( + recommendation_id=recommendation_id, + recommendation=recommendation, + db=db, + ) + except ValueError: + raise HTTPException(status_code=404, detail="The recommendation does not exist") @module.router.delete( From 671d3e5af34b09bf0420465a3a34f31f3fb4c1b1 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:57:53 +0100 Subject: [PATCH 16/21] return null instead of raising error --- app/modules/recommendation/cruds_recommendation.py | 8 ++------ .../recommendation/endpoints_recommendation.py | 12 ++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 7819a2742..4ce043e9d 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -1,7 +1,6 @@ from typing import Sequence from sqlalchemy import delete, select, update -from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession from app.modules.recommendation import models_recommendation, schemas_recommendation @@ -62,13 +61,10 @@ async def delete_recommendation( async def get_recommendation_by_id( recommendation_id: str, db: AsyncSession, -) -> models_recommendation.Recommendation: +) -> models_recommendation.Recommendation | None: result = await db.execute( select(models_recommendation.Recommendation).where( models_recommendation.Recommendation.id == recommendation_id ) ) - try: - return result.scalars().one() - except NoResultFound: - raise ValueError + return result.scalars().one_or_none() diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 9ea9b3a33..362c57052 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -167,12 +167,12 @@ async def create_recommendation_image( **This endpoint is only usable by members of the group BDE** """ - try: - await cruds_recommendation.get_recommendation_by_id( - recommendation_id=recommendation_id, - db=db, - ) - except ValueError: + recommendation = await cruds_recommendation.get_recommendation_by_id( + recommendation_id=recommendation_id, + db=db, + ) + + if not recommendation: raise HTTPException(status_code=404, detail="The recommendation does not exist") await save_file_as_data( From 463bb115ac25613644698255f9d0b2467d58ba2a Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 16 Mar 2024 05:29:31 +0100 Subject: [PATCH 17/21] apply ruff rules and use new datetime type --- app/modules/recommendation/cruds_recommendation.py | 12 ++++++------ .../recommendation/endpoints_recommendation.py | 2 +- app/modules/recommendation/models_recommendation.py | 5 +++-- migrations/versions/4-recommendation.py | 11 +++++------ tests/test_recommendation.py | 11 ++++++----- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/modules/recommendation/cruds_recommendation.py b/app/modules/recommendation/cruds_recommendation.py index 4ce043e9d..0e1dd4100 100644 --- a/app/modules/recommendation/cruds_recommendation.py +++ b/app/modules/recommendation/cruds_recommendation.py @@ -1,4 +1,4 @@ -from typing import Sequence +from collections.abc import Sequence from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -33,7 +33,7 @@ async def update_recommendation( result = await db.execute( update(models_recommendation.Recommendation) .where(models_recommendation.Recommendation.id == recommendation_id) - .values(**recommendation.model_dump(exclude_none=True)) + .values(**recommendation.model_dump(exclude_none=True)), ) if result.rowcount == 1: await db.commit() @@ -48,8 +48,8 @@ async def delete_recommendation( ): result = await db.execute( delete(models_recommendation.Recommendation).where( - models_recommendation.Recommendation.id == recommendation_id - ) + models_recommendation.Recommendation.id == recommendation_id, + ), ) if result.rowcount == 1: await db.commit() @@ -64,7 +64,7 @@ async def get_recommendation_by_id( ) -> models_recommendation.Recommendation | None: result = await db.execute( select(models_recommendation.Recommendation).where( - models_recommendation.Recommendation.id == recommendation_id - ) + models_recommendation.Recommendation.id == recommendation_id, + ), ) return result.scalars().one_or_none() diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 362c57052..72bb240a2 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -76,7 +76,7 @@ async def create_recommendation( ) return await cruds_recommendation.create_recommendation( - recommendation=recommendation_db, db=db + recommendation=recommendation_db, db=db, ) diff --git a/app/modules/recommendation/models_recommendation.py b/app/modules/recommendation/models_recommendation.py index f8c7e388f..4674e10de 100644 --- a/app/modules/recommendation/models_recommendation.py +++ b/app/modules/recommendation/models_recommendation.py @@ -1,16 +1,17 @@ from datetime import datetime -from sqlalchemy import DateTime, String +from sqlalchemy import String from sqlalchemy.orm import Mapped, mapped_column from app.database import Base +from app.utils.types.datetime import TZDateTime class Recommendation(Base): __tablename__ = "recommendation" id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False) - creation: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + creation: Mapped[datetime] = mapped_column(TZDateTime, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) code: Mapped[str | None] = mapped_column(String, nullable=True) diff --git a/migrations/versions/4-recommendation.py b/migrations/versions/4-recommendation.py index dee1d40a8..545e45a8d 100644 --- a/migrations/versions/4-recommendation.py +++ b/migrations/versions/4-recommendation.py @@ -6,17 +6,16 @@ """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision: str = "d233bd29c521" -down_revision: Union[str, None] = "f17e6182b0a9" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "f17e6182b0a9" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/tests/test_recommendation.py b/tests/test_recommendation.py index cf73d8677..aed4a3175 100644 --- a/tests/test_recommendation.py +++ b/tests/test_recommendation.py @@ -1,16 +1,17 @@ import uuid -from datetime import datetime +from datetime import UTC, datetime +from pathlib import Path import pytest_asyncio from app.core.groups.groups_type import GroupType from app.modules.recommendation import models_recommendation -from tests.commons import event_loop # noqa from tests.commons import ( add_object_to_db, client, create_api_access_token, create_user_with_groups, + event_loop, # noqa ) @@ -29,7 +30,7 @@ async def init_objects(): global recommendation recommendation = models_recommendation.Recommendation( id=str(uuid.uuid4()), - creation=datetime.now(), + creation=datetime.now(UTC), title="Un titre", code="Un code", summary="Un résumé", @@ -39,7 +40,7 @@ async def init_objects(): def test_create_picture(): - with open("assets/images/default_recommendation.png", "rb") as image: + with Path.open("assets/images/default_recommendation.png", "rb") as image: response = client.post( f"/recommendation/recommendations/{recommendation.id}/picture", files={"image": ("recommendation.png", image, "image/png")}, @@ -49,7 +50,7 @@ def test_create_picture(): def test_create_picture_for_non_existing_recommendation(): - with open("assets/images/default_recommendation.png", "rb") as image: + with Path.open("assets/images/default_recommendation.png", "rb") as image: response = client.post( "/recommendation/recommendations/false_id/picture", files={"image": ("recommendation.png", image, "image/png")}, From df71cc80bf6ef9715e6bd23d3cdc067e8ce0666c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 16 Mar 2024 06:08:43 +0100 Subject: [PATCH 18/21] fix migration file --- .../{3-pydantic_v2.py => 2-pydantic_v2.py} | 0 .../{4-fix_datetime.py => 3-fix_datetime.py} | 0 migrations/versions/4-recommendation.py | 14 +++++++------- 3 files changed, 7 insertions(+), 7 deletions(-) rename migrations/versions/{3-pydantic_v2.py => 2-pydantic_v2.py} (100%) rename migrations/versions/{4-fix_datetime.py => 3-fix_datetime.py} (100%) diff --git a/migrations/versions/3-pydantic_v2.py b/migrations/versions/2-pydantic_v2.py similarity index 100% rename from migrations/versions/3-pydantic_v2.py rename to migrations/versions/2-pydantic_v2.py diff --git a/migrations/versions/4-fix_datetime.py b/migrations/versions/3-fix_datetime.py similarity index 100% rename from migrations/versions/4-fix_datetime.py rename to migrations/versions/3-fix_datetime.py diff --git a/migrations/versions/4-recommendation.py b/migrations/versions/4-recommendation.py index 545e45a8d..a894c4b69 100644 --- a/migrations/versions/4-recommendation.py +++ b/migrations/versions/4-recommendation.py @@ -1,8 +1,8 @@ -"""recommendation +"""add migration file for recommendation module -Revision ID: d233bd29c521 -Revises: f17e6182b0a9 -Create Date: 2024-02-23 19:12:25.372249 +Revision ID: 3f9843f165e9 +Revises: 99a2c70e4a24 +Create Date: 2024-03-16 06:02:34.664485 """ @@ -12,8 +12,8 @@ from alembic import op # revision identifiers, used by Alembic. -revision: str = "d233bd29c521" -down_revision: str | None = "f17e6182b0a9" +revision: str = "3f9843f165e9" +down_revision: str | None = "99a2c70e4a24" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -23,7 +23,7 @@ def upgrade() -> None: op.create_table( "recommendation", sa.Column("id", sa.String(), nullable=False), - sa.Column("creation", sa.DateTime(timezone=True), nullable=False), + sa.Column("creation", sa.DateTime(timezone=False), nullable=False), sa.Column("title", sa.String(), nullable=False), sa.Column("code", sa.String(), nullable=True), sa.Column("summary", sa.String(), nullable=False), From 615e973fb0e9e44bb59ce83f66b2be761a62bd2e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 16 Mar 2024 06:46:18 +0100 Subject: [PATCH 19/21] use only UTC --- app/modules/recommendation/endpoints_recommendation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 72bb240a2..84af589f4 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -1,6 +1,5 @@ import uuid -from datetime import datetime -from zoneinfo import ZoneInfo +from datetime import UTC, datetime from fastapi import APIRouter, Depends, File, HTTPException, UploadFile from fastapi.responses import FileResponse @@ -71,12 +70,13 @@ async def create_recommendation( recommendation_db = models_recommendation.Recommendation( id=str(uuid.uuid4()), - creation=datetime.now(ZoneInfo(settings.TIMEZONE)), + creation=datetime.now(UTC), **recommendation.model_dump(), ) return await cruds_recommendation.create_recommendation( - recommendation=recommendation_db, db=db, + recommendation=recommendation_db, + db=db, ) From b0e2ddafc54564e5b0a66cf7e51d5eb79ff3b6e0 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:15:40 +0100 Subject: [PATCH 20/21] validate id to avoid security issue --- app/modules/recommendation/endpoints_recommendation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 84af589f4..8342dd796 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -137,12 +137,21 @@ async def delete_recommendation( ) async def read_recommendation_image( recommendation_id: str, + db: AsyncSession = Depends(get_db), ): """ Get the image of a recommendation. **The user must be authenticated to use this endpoint** """ + recommendation = await cruds_recommendation.get_recommendation_by_id( + recommendation_id=recommendation_id, + db=db, + ) + + if not recommendation: + raise HTTPException(status_code=404, detail="The recommendation does not exist") + return get_file_from_data( default_asset="assets/images/default_recommendation.png", directory="recommendations", From a7a3c2fe44704b73d6df85a3391427b9fbddad80 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:16:52 +0100 Subject: [PATCH 21/21] check that the user is a member --- app/modules/recommendation/endpoints_recommendation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/modules/recommendation/endpoints_recommendation.py b/app/modules/recommendation/endpoints_recommendation.py index 8342dd796..b4755f500 100644 --- a/app/modules/recommendation/endpoints_recommendation.py +++ b/app/modules/recommendation/endpoints_recommendation.py @@ -138,6 +138,7 @@ async def delete_recommendation( async def read_recommendation_image( recommendation_id: str, db: AsyncSession = Depends(get_db), + user: models_core.CoreUser = Depends(is_user_a_member), ): """ Get the image of a recommendation.