minor: org pydantic model cleanup

Contact models also updated since they are now fully incorporated into orgs.

Issue #9
This commit is contained in:
Chris Milne 2026-05-27 16:51:46 +01:00
parent 216836e2fd
commit 4bf5933376
3 changed files with 47 additions and 90 deletions

View file

@ -9,7 +9,6 @@ from typing import Optional
from pydantic import EmailStr, ConfigDict from pydantic import EmailStr, ConfigDict
from src.organisation.constants import ContactType
from src.schemas import CustomBaseModel from src.schemas import CustomBaseModel
@ -25,50 +24,11 @@ class ContactAddress(CustomBaseModel):
postal_code: Optional[str] = None postal_code: Optional[str] = None
class ContactContactGetResponse(CustomBaseModel): class ContactModel(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):
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
first_name: Optional[str] = None first_name: Optional[str] = None
last_name: Optional[str] = None last_name: Optional[str] = None
phonenumber: Optional[str] = None phonenumber: Optional[str] = None
vat_number: 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): address: ContactAddress
name: str
contact_types: list[ContactType]

View file

@ -19,6 +19,7 @@ from fastapi.params import Query
from psycopg.errors import UniqueViolation from psycopg.errors import UniqueViolation
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from contact.schemas import ContactModel
from src.exceptions import UnprocessableContent, Conflict from src.exceptions import UnprocessableContent, Conflict
from src.contact.models import Contact from src.contact.models import Contact
from src.contact.schemas import ContactAddress 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.dependencies import org_model_body_dependency
from src.organisation.constants import ContactType from src.organisation.constants import ContactType
from src.organisation.models import Organisation as Org from src.organisation.models import Organisation as Org
from src.organisation.schemas import OrgOrgPostRequest, OrgQuestionnairePatchRequest, OrgStatusPatchRequest, \ from src.organisation.schemas import OrgPostOrgRequest, OrgPatchQuestionnaireRequest, OrgPatchStatusRequest, \
OrgContactPatchRequest, \ OrgPatchContactRequest, \
OrgUserPostRequest, OrgUserGetResponse, OrgContactGetResponse, OrgOrgGetResponse, OrgRootPatchRequest, \ OrgPostUserRequest, OrgGetUserResponse, OrgGetContactResponse, OrgGetOrgResponse, OrgPatchRootRequest, \
OrgGroupGetResponse, OrgUserDeleteRequest, OrgDeleteOrgRequest OrgGetGroupResponse, OrgDeleteUserRequest, OrgDeleteOrgRequest
router = APIRouter( 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): async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
response = { response = {
"name": org_model.name, "name": org_model.name,
@ -58,7 +59,7 @@ async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
@router.post("/") @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: if request_model.intake_questionnaire:
intake_questionnaire = request_model.intake_questionnaire.model_dump() intake_questionnaire = request_model.intake_questionnaire.model_dump()
else: else:
@ -85,7 +86,7 @@ async def create_org(db: db_dependency, user_model: user_model_claims_dependency
@router.patch("/questionnaire") @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. Route for updating questionnaire.
The partial bool allows for submission of partially completed questionnaire and/or 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") @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 org_model.status = request_model.status
db.commit() 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): async def get_users(org_model: org_model_root_claim_query_dependency):
return {"users": [user.email for user in org_model.user_rel]} return {"users": [user.email for user in org_model.user_rel]}
@router.post("/users") @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: if user_model in org_model.user_rel:
raise Conflict(message="User already a part of this organisation") raise Conflict(message="User already a part of this organisation")
org_model.user_rel.append(user_model) 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) @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 org_model.root_user_rel = user_model
db.commit() 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): async def get_org_groups(org_model: org_model_root_claim_query_dependency):
return {"groups": [group.name for group in org_model.group_rel]} return {"groups": [group.name for group in org_model.group_rel]}
@router.delete("/user", status_code=status.HTTP_204_NO_CONTENT) @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: if user_model not in org_model.user_rel:
return return
@ -146,7 +147,7 @@ async def remove_user_from_org(db: db_dependency, org_model: org_model_root_clai
db.commit() 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()]): async def get_contact(org_model: org_model_root_claim_query_dependency, contact_type: Annotated[ContactType, Query()]):
match contact_type: match contact_type:
case "billing": case "billing":
@ -161,14 +162,14 @@ async def get_contact(org_model: org_model_root_claim_query_dependency, contact_
if contact_model is None: if contact_model is None:
raise ContactNotFoundException() raise ContactNotFoundException()
return OrgContactGetResponse.model_construct( address = ContactAddress.model_validate(contact_model)
**contact_model.__dict__, contact_response = ContactModel.model_construct(**contact_model.__dict__, address=address)
address=ContactAddress.model_validate(contact_model)
) return {"contact": contact_response}
@router.patch("/contact", response_model=OrgContactGetResponse) @router.patch("/contact", response_model=OrgGetContactResponse)
async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgContactPatchRequest): async def update_contact(db: db_dependency, org_model: org_model_root_claim_body_dependency, request_model: OrgPatchContactRequest):
match request_model.contact_type: match request_model.contact_type:
case "billing": case "billing":
contact_model = org_model.billing_contact_rel 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") raise UnprocessableContent("Invalid keys in update request")
db.flush() db.flush()
response = OrgContactGetResponse.model_construct( address = ContactAddress.model_validate(contact_model)
**contact_model.__dict__, contact_response = ContactModel.model_construct(**contact_model.__dict__, address=address)
address=ContactAddress.model_validate(contact_model)
)
db.commit() db.commit()
return response return {"contact": contact_response}

View file

@ -10,8 +10,11 @@ from typing import Optional
from pydantic import EmailStr, ConfigDict from pydantic import EmailStr, ConfigDict
from src.schemas import CustomBaseModel 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.organisation.constants import Status, ContactType
from src.contact.schemas import ContactAddress
class OrgQuestionnaire(CustomBaseModel): class OrgQuestionnaire(CustomBaseModel):
question_one: str question_one: str
@ -21,18 +24,19 @@ class OrgQuestionnaire(CustomBaseModel):
class OrgIDMixin(CustomBaseModel): class OrgIDMixin(CustomBaseModel):
organisation_id: int organisation_id: int
class OrgOrgPostRequest(CustomBaseModel):
class OrgPostOrgRequest(CustomBaseModel):
name: str name: str
intake_questionnaire: Optional[OrgQuestionnaire] = None intake_questionnaire: Optional[OrgQuestionnaire] = None
class OrgQuestionnairePatchRequest(OrgIDMixin): class OrgPatchQuestionnaireRequest(OrgIDMixin):
intake_questionnaire: OrgQuestionnaire intake_questionnaire: OrgQuestionnaire
partial: bool partial: bool
class OrgStatusPatchRequest(OrgIDMixin): class OrgPatchStatusRequest(OrgIDMixin):
status: Status status: Status
class OrgContactPatchRequest(OrgIDMixin): class OrgPatchContactRequest(OrgIDMixin):
contact_type: ContactType contact_type: ContactType
email: Optional[EmailStr] = None email: Optional[EmailStr] = None
@ -48,33 +52,27 @@ class OrgContactPatchRequest(OrgIDMixin):
country_code: Optional[str] = None country_code: Optional[str] = None
postal_code: Optional[str] = None postal_code: Optional[str] = None
class OrgUserPostRequest(OrgIDMixin): class OrgPostUserRequest(OrgIDMixin, UserIDMixin):
user_id: int pass
class OrgUserDeleteRequest(OrgIDMixin): class OrgDeleteUserRequest(OrgIDMixin, UserIDMixin):
user_id: int pass
class OrgRootPatchRequest(OrgIDMixin): class OrgPatchRootRequest(OrgIDMixin, UserIDMixin):
user_id: int pass
class OrgUserGetResponse(CustomBaseModel): class OrgGetUserResponse(CustomBaseModel):
users: list[str] users: list[str]
class OrgGroupGetResponse(CustomBaseModel): class OrgGetGroupResponse(CustomBaseModel):
groups: list[str] groups: list[str]
class OrgContactGetResponse(CustomBaseModel): class OrgGetContactResponse(CustomBaseModel):
model_config = ConfigDict(from_attributes=True, extra="ignore") model_config = ConfigDict(from_attributes=True, extra="ignore")
email: Optional[str] = None contact: ContactModel
first_name: Optional[str] = None
last_name: Optional[str] = None
phonenumber: Optional[str] = None
vat_number: Optional[str] = None
address: ContactAddress class OrgGetOrgResponse(CustomBaseModel):
class OrgOrgGetResponse(CustomBaseModel):
name: str name: str
status: Status status: Status
root_user: Optional[str] = None root_user: Optional[str] = None