From 4bf59333763443a0e936e7d668447542e3835b79 Mon Sep 17 00:00:00 2001 From: luxferre Date: Wed, 27 May 2026 16:51:46 +0100 Subject: [PATCH] minor: org pydantic model cleanup Contact models also updated since they are now fully incorporated into orgs. Issue #9 --- src/contact/schemas.py | 44 ++------------------------------- src/organisation/router.py | 49 ++++++++++++++++++------------------- src/organisation/schemas.py | 44 ++++++++++++++++----------------- 3 files changed, 47 insertions(+), 90 deletions(-) diff --git a/src/contact/schemas.py b/src/contact/schemas.py index c3d705c..b5103f0 100644 --- a/src/contact/schemas.py +++ b/src/contact/schemas.py @@ -9,7 +9,6 @@ from typing import Optional from pydantic import EmailStr, ConfigDict -from src.organisation.constants import ContactType from src.schemas import CustomBaseModel @@ -25,50 +24,11 @@ class ContactAddress(CustomBaseModel): postal_code: Optional[str] = None -class ContactContactGetResponse(CustomBaseModel): - email: str - first_name: str - last_name: str - phonenumber: str - vat_number: Optional[str] = None - -class ContactAddressGetResponse(CustomBaseModel): - post_office_box_number: Optional[str] = None - street_address: Optional[str] = None # If using a PO box, there would be no street address - street_address_line_2: Optional[str] = None - locality: str - address_region: Optional[str] = None - country_code: str - postal_code: str - -class ContactContactPostRequest(CustomBaseModel): - email: EmailStr - first_name: str - last_name: str - phonenumber: str - vat_number: Optional[str] = None - post_office_box_number: Optional[str] = None - street_address: Optional[str] = None - street_address_line_2: Optional[str] = None - locality: str - address_region: Optional[str] = None - country_code: str - postal_code: str - -class ContactUpdateRequest(CustomBaseModel): +class ContactModel(CustomBaseModel): email: Optional[EmailStr] = None first_name: Optional[str] = None last_name: Optional[str] = None phonenumber: Optional[str] = None vat_number: Optional[str] = None - post_office_box_number: Optional[str] = None - street_address: Optional[str] = None - street_address_line_2: Optional[str] = None - locality: Optional[str] = None - address_region: Optional[str] = None - country_code: Optional[str] = None - postal_code: Optional[str] = None -class ContactOrgGetResponse(CustomBaseModel): - name: str - contact_types: list[ContactType] + address: ContactAddress diff --git a/src/organisation/router.py b/src/organisation/router.py index e1193f0..c26bc2f 100644 --- a/src/organisation/router.py +++ b/src/organisation/router.py @@ -19,6 +19,7 @@ from fastapi.params import Query from psycopg.errors import UniqueViolation from sqlalchemy.exc import IntegrityError +from contact.schemas import ContactModel from src.exceptions import UnprocessableContent, Conflict from src.contact.models import Contact from src.contact.schemas import ContactAddress @@ -31,10 +32,10 @@ from src.auth.dependencies import super_admin_dependency, org_model_root_claim_q from src.organisation.dependencies import org_model_body_dependency from src.organisation.constants import ContactType from src.organisation.models import Organisation as Org -from src.organisation.schemas import OrgOrgPostRequest, OrgQuestionnairePatchRequest, OrgStatusPatchRequest, \ - OrgContactPatchRequest, \ - OrgUserPostRequest, OrgUserGetResponse, OrgContactGetResponse, OrgOrgGetResponse, OrgRootPatchRequest, \ - OrgGroupGetResponse, OrgUserDeleteRequest, OrgDeleteOrgRequest +from src.organisation.schemas import OrgPostOrgRequest, OrgPatchQuestionnaireRequest, OrgPatchStatusRequest, \ + OrgPatchContactRequest, \ + OrgPostUserRequest, OrgGetUserResponse, OrgGetContactResponse, OrgGetOrgResponse, OrgPatchRootRequest, \ + OrgGetGroupResponse, OrgDeleteUserRequest, OrgDeleteOrgRequest router = APIRouter( @@ -43,7 +44,7 @@ router = APIRouter( ) -@router.get("/id", response_model=OrgOrgGetResponse) +@router.get("/id", response_model=OrgGetOrgResponse) async def get_org_by_id(org_model: org_model_root_claim_query_dependency): response = { "name": org_model.name, @@ -58,7 +59,7 @@ async def get_org_by_id(org_model: org_model_root_claim_query_dependency): @router.post("/") -async def create_org(db: db_dependency, user_model: user_model_claims_dependency, request_model: OrgOrgPostRequest): +async def create_org(db: db_dependency, user_model: user_model_claims_dependency, request_model: OrgPostOrgRequest): if request_model.intake_questionnaire: intake_questionnaire = request_model.intake_questionnaire.model_dump() else: @@ -85,7 +86,7 @@ async def create_org(db: db_dependency, user_model: user_model_claims_dependency @router.patch("/questionnaire") -async def update_questionnaire(db: db_dependency, org_model: org_model_root_claim_query_dependency, request_model: OrgQuestionnairePatchRequest): +async def update_questionnaire(db: db_dependency, org_model: org_model_root_claim_query_dependency, request_model: OrgPatchQuestionnaireRequest): """ Route for updating questionnaire. The partial bool allows for submission of partially completed questionnaire and/or @@ -101,19 +102,19 @@ async def update_questionnaire(db: db_dependency, org_model: org_model_root_clai @router.patch("/status") -async def update_status(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgStatusPatchRequest): +async def update_status(db: db_dependency, org_model: org_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchStatusRequest): org_model.status = request_model.status db.commit() -@router.get("/users", response_model=OrgUserGetResponse) +@router.get("/users", response_model=OrgGetUserResponse) async def get_users(org_model: org_model_root_claim_query_dependency): return {"users": [user.email for user in org_model.user_rel]} @router.post("/users") -async def add_user_to_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgUserPostRequest): +async def add_user_to_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgPostUserRequest): if user_model in org_model.user_rel: raise Conflict(message="User already a part of this organisation") org_model.user_rel.append(user_model) @@ -127,18 +128,18 @@ async def delete_organisation_by_id(db: db_dependency, org_model: org_model_body @router.patch("/root_user", status_code=status.HTTP_204_NO_CONTENT) -async def update_root_user(db: db_dependency, org_model: org_model_body_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: OrgRootPatchRequest): +async def update_root_user(db: db_dependency, org_model: org_model_body_dependency, user_model: user_model_body_dependency, su: super_admin_dependency, request_model: OrgPatchRootRequest): org_model.root_user_rel = user_model db.commit() -@router.get("/groups", response_model=OrgGroupGetResponse) +@router.get("/groups", response_model=OrgGetGroupResponse) async def get_org_groups(org_model: org_model_root_claim_query_dependency): return {"groups": [group.name for group in org_model.group_rel]} @router.delete("/user", status_code=status.HTTP_204_NO_CONTENT) -async def remove_user_from_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgUserDeleteRequest): +async def remove_user_from_org(db: db_dependency, org_model: org_model_root_claim_body_dependency, user_model: user_model_body_dependency, request_model: OrgDeleteUserRequest): if user_model not in org_model.user_rel: return @@ -146,7 +147,7 @@ async def remove_user_from_org(db: db_dependency, org_model: org_model_root_clai db.commit() -@router.get("/contact", response_model=OrgContactGetResponse) +@router.get("/contact", response_model=OrgGetContactResponse) async def get_contact(org_model: org_model_root_claim_query_dependency, contact_type: Annotated[ContactType, Query()]): match contact_type: case "billing": @@ -161,14 +162,14 @@ async def get_contact(org_model: org_model_root_claim_query_dependency, contact_ if contact_model is None: raise ContactNotFoundException() - return OrgContactGetResponse.model_construct( - **contact_model.__dict__, - address=ContactAddress.model_validate(contact_model) - ) + address = ContactAddress.model_validate(contact_model) + contact_response = ContactModel.model_construct(**contact_model.__dict__, address=address) + + return {"contact": contact_response} -@router.patch("/contact", response_model=OrgContactGetResponse) -async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgContactPatchRequest): +@router.patch("/contact", response_model=OrgGetContactResponse) +async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgPatchContactRequest): match request_model.contact_type: case "billing": contact_model = org_model.billing_contact_rel @@ -190,11 +191,9 @@ async def update_contact(db: db_dependency, org_model: org_model_root_claim_body raise UnprocessableContent("Invalid keys in update request") db.flush() - response = OrgContactGetResponse.model_construct( - **contact_model.__dict__, - address=ContactAddress.model_validate(contact_model) - ) + address = ContactAddress.model_validate(contact_model) + contact_response = ContactModel.model_construct(**contact_model.__dict__, address=address) db.commit() - return response + return {"contact": contact_response} diff --git a/src/organisation/schemas.py b/src/organisation/schemas.py index 3efb67d..d727b98 100644 --- a/src/organisation/schemas.py +++ b/src/organisation/schemas.py @@ -10,8 +10,11 @@ from typing import Optional from pydantic import EmailStr, ConfigDict from src.schemas import CustomBaseModel +from src.contact.schemas import ContactModel +from src.user.schemas import UserIDMixin + from src.organisation.constants import Status, ContactType -from src.contact.schemas import ContactAddress + class OrgQuestionnaire(CustomBaseModel): question_one: str @@ -21,18 +24,19 @@ class OrgQuestionnaire(CustomBaseModel): class OrgIDMixin(CustomBaseModel): organisation_id: int -class OrgOrgPostRequest(CustomBaseModel): + +class OrgPostOrgRequest(CustomBaseModel): name: str intake_questionnaire: Optional[OrgQuestionnaire] = None -class OrgQuestionnairePatchRequest(OrgIDMixin): +class OrgPatchQuestionnaireRequest(OrgIDMixin): intake_questionnaire: OrgQuestionnaire partial: bool -class OrgStatusPatchRequest(OrgIDMixin): +class OrgPatchStatusRequest(OrgIDMixin): status: Status -class OrgContactPatchRequest(OrgIDMixin): +class OrgPatchContactRequest(OrgIDMixin): contact_type: ContactType email: Optional[EmailStr] = None @@ -48,33 +52,27 @@ class OrgContactPatchRequest(OrgIDMixin): country_code: Optional[str] = None postal_code: Optional[str] = None -class OrgUserPostRequest(OrgIDMixin): - user_id: int +class OrgPostUserRequest(OrgIDMixin, UserIDMixin): + pass -class OrgUserDeleteRequest(OrgIDMixin): - user_id: int +class OrgDeleteUserRequest(OrgIDMixin, UserIDMixin): + pass -class OrgRootPatchRequest(OrgIDMixin): - user_id: int +class OrgPatchRootRequest(OrgIDMixin, UserIDMixin): + pass -class OrgUserGetResponse(CustomBaseModel): +class OrgGetUserResponse(CustomBaseModel): users: list[str] -class OrgGroupGetResponse(CustomBaseModel): +class OrgGetGroupResponse(CustomBaseModel): groups: list[str] -class OrgContactGetResponse(CustomBaseModel): +class OrgGetContactResponse(CustomBaseModel): model_config = ConfigDict(from_attributes=True, extra="ignore") - email: Optional[str] = None - first_name: Optional[str] = None - last_name: Optional[str] = None - phonenumber: Optional[str] = None - vat_number: Optional[str] = None + contact: ContactModel - address: ContactAddress - -class OrgOrgGetResponse(CustomBaseModel): +class OrgGetOrgResponse(CustomBaseModel): name: str status: Status root_user: Optional[str] = None @@ -83,4 +81,4 @@ class OrgOrgGetResponse(CustomBaseModel): security_contact: Optional[str] = None class OrgDeleteOrgRequest(OrgIDMixin): - pass \ No newline at end of file + pass