From aeceac3e96e6cc12bb974c679fc7bc4fc26d98cb Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Tue, 29 Oct 2024 14:01:16 -0300 Subject: [PATCH 1/2] Add Terms of Use acceptance feature for User model --- app/models.py | 3 +++ app/routers/frontend.py | 20 ++++++++++++++++++++ migrations/app/35_20241029140029_update.py | 14 ++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 migrations/app/35_20241029140029_update.py diff --git a/app/models.py b/app/models.py index 0096d066..73a2eecd 100644 --- a/app/models.py +++ b/app/models.py @@ -41,6 +41,9 @@ class User(Model): is_2fa_required = fields.BooleanField(default=False) is_2fa_activated = fields.BooleanField(default=False) is_ergon_validation_required = fields.BooleanField(default=False) + # Terms of use + is_use_terms_accepted = fields.BooleanField(default=False) + use_terms_accepted_at = fields.DatetimeField(null=True) # Metadata is_active = fields.BooleanField(default=True) is_superuser = fields.BooleanField(default=False) diff --git a/app/routers/frontend.py b/app/routers/frontend.py index 6bc729ea..3ce243cc 100644 --- a/app/routers/frontend.py +++ b/app/routers/frontend.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import asyncio +import datetime from typing import Annotated, List from fastapi import APIRouter, Depends, Request from fastapi_limiter.depends import RateLimiter @@ -45,6 +46,7 @@ async def get_user_info( "role": user.role.job_title if user.role else None, "email": user.email, "username": user.username, + "is_use_terms_accepted": user.is_use_terms_accepted, "cpf": cpf, } @@ -150,6 +152,24 @@ async def get_patient_encounters( return [] +@router_request( + method="POST", + router=router, + path="/user/accept-terms/", + response_model=bool, +) +async def accept_use_terms( + user: Annotated[User, Depends(assert_user_is_active)], + request: Request, +) -> List[Encounter]: + + user.is_use_terms_accepted = True + user.use_terms_accepted_at = datetime.datetime.now() + await user.save() + + return user + + @router.get("/patient/filter_tags") async def get_filter_tags(_: Annotated[User, Depends(assert_user_is_active)]) -> List[str]: return [ diff --git a/migrations/app/35_20241029140029_update.py b/migrations/app/35_20241029140029_update.py new file mode 100644 index 00000000..e131aab1 --- /dev/null +++ b/migrations/app/35_20241029140029_update.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "user" ADD "use_terms_accepted_at" TIMESTAMPTZ; + ALTER TABLE "user" ADD "is_use_terms_accepted" BOOL NOT NULL DEFAULT False;""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE "user" DROP COLUMN "use_terms_accepted_at"; + ALTER TABLE "user" DROP COLUMN "is_use_terms_accepted";""" From 185ce215faf06c7fca58290c5a6be57183b93559 Mon Sep 17 00:00:00 2001 From: Pedro Nascimento Date: Tue, 29 Oct 2024 14:40:24 -0300 Subject: [PATCH 2/2] Applying Models in Configuration --- app/enums.py | 4 +++ app/routers/frontend.py | 58 +++++++++++++++++++++++++++-------------- app/types/errors.py | 10 +++++-- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/app/enums.py b/app/enums.py index f380d187..767ba04f 100644 --- a/app/enums.py +++ b/app/enums.py @@ -18,6 +18,10 @@ class LoginErrorEnum(str, Enum): INACTIVE_EMPLOYEE = "inactive_employee" REQUIRE_2FA = "require_2fa" +class AcceptTermsEnum(str, Enum): + SUCCESS = "success" + FAILURE = "failure" + class AccessErrorEnum(str, Enum): NOT_FOUND = "NOT_FOUND" diff --git a/app/routers/frontend.py b/app/routers/frontend.py index 03912a01..70459a7e 100644 --- a/app/routers/frontend.py +++ b/app/routers/frontend.py @@ -4,6 +4,7 @@ from typing import Annotated, List from fastapi import APIRouter, Depends, Request from fastapi_limiter.depends import RateLimiter +from fastapi.responses import JSONResponse from app.decorators import router_request from app.dependencies import assert_user_is_active, assert_cpf_is_valid @@ -14,6 +15,7 @@ Encounter, UserInfo, ) +from app.types.errors import AcceptTermsEnum from app.utils import read_bq, validate_user_access_to_patient_data from app.config import ( BIGQUERY_PROJECT, @@ -24,7 +26,8 @@ REQUEST_LIMIT_WINDOW_SIZE, ) from app.types.errors import ( - AccessErrorModel + AccessErrorModel, + TermAcceptanceErrorModel ) router = APIRouter(prefix="/frontend", tags=["Frontend Application"]) @@ -51,6 +54,41 @@ async def get_user_info( } +@router_request( + method="POST", + router=router, + path="/user/accept-terms/", + response_model=TermAcceptanceErrorModel, + responses={ + 500: {"model": TermAcceptanceErrorModel}, + }, +) +async def accept_use_terms( + user: Annotated[User, Depends(assert_user_is_active)], + request: Request, +) -> TermAcceptanceErrorModel: + + try: + user.is_use_terms_accepted = True + user.use_terms_accepted_at = datetime.datetime.now() + await user.save() + return JSONResponse( + status_code=200, + content={ + "message": "Success", + "type": AcceptTermsEnum.SUCCESS, + }, + ) + except Exception: + return JSONResponse( + status_code=500, + content={ + "message": "Patient not found", + "type": AcceptTermsEnum.FAILURE, + }, + ) + + @router_request( method="GET", router=router, @@ -152,24 +190,6 @@ async def get_patient_encounters( return [] -@router_request( - method="POST", - router=router, - path="/user/accept-terms/", - response_model=bool, -) -async def accept_use_terms( - user: Annotated[User, Depends(assert_user_is_active)], - request: Request, -) -> List[Encounter]: - - user.is_use_terms_accepted = True - user.use_terms_accepted_at = datetime.datetime.now() - await user.save() - - return user - - @router.get("/patient/filter_tags") async def get_filter_tags(_: Annotated[User, Depends(assert_user_is_active)]) -> List[str]: return [ diff --git a/app/types/errors.py b/app/types/errors.py index a8b76f08..069dfb3c 100644 --- a/app/types/errors.py +++ b/app/types/errors.py @@ -3,7 +3,8 @@ from app.enums import ( LoginErrorEnum, - AccessErrorEnum + AccessErrorEnum, + AcceptTermsEnum ) @@ -14,4 +15,9 @@ class AuthenticationErrorModel(BaseModel): class AccessErrorModel(BaseModel): message: str - type: AccessErrorEnum \ No newline at end of file + type: AccessErrorEnum + + +class TermAcceptanceErrorModel(BaseModel): + message: str + type: AcceptTermsEnum