fix: ty compliant & issues from change to mapped columns
All checks were successful
ci / ruff (push) Successful in 3s
ci / ty (push) Successful in 15s
ci / tests (push) Successful in 17s

This commit is contained in:
Chris Milne 2026-06-22 11:23:24 +01:00
parent 55927946c7
commit 58e7ae6c5c
31 changed files with 271 additions and 254 deletions

View file

@ -14,12 +14,12 @@ class Status(StrEnum):
Enumeration of organisation statuses.
Attributes:
PARTIAL(str): Organisation has been created but questionnaire hasn't been submitted.
SUBMITTED (str): Questionnaire submitted but not approved.
REMEDIATION (str): Questionnaire submitted but requires revisions.
APPROVED (str): Questionnaire has been approved by an admin.
REJECTED (str): Questionnaire has been rejected by an admin.
REMOVED (str): Organisation has been removed.
PARTIAL(str): Organisation has been created but questionnaire hasn't been submitted.
SUBMITTED (str): Questionnaire submitted but not approved.
REMEDIATION (str): Questionnaire submitted but requires revisions.
APPROVED (str): Questionnaire has been approved by an admin.
REJECTED (str): Questionnaire has been rejected by an admin.
REMOVED (str): Organisation has been removed.
"""
PARTIAL = auto()
@ -47,9 +47,9 @@ class ContactType(StrEnum):
Enumeration of organisation contact types.
Attributes:
BILLING(str): Billing contact.
SECURITY (str): Security contact.
OWNER (str): Owner contact.
BILLING(str): Billing contact.
SECURITY (str): Security contact.
OWNER (str): Owner contact.
"""
BILLING = auto()

View file

@ -17,19 +17,17 @@ from src.organisation.models import Organisation as Org
from src.organisation.exceptions import OrgNotFoundException
def get_org_model_query(
db: db_dependency, org_id: Annotated[int, Query(gt=0)]
) -> type[Org]:
def get_org_model_query(db: db_dependency, org_id: Annotated[int, Query(gt=0)]) -> Org:
org_model = db.get(Org, org_id)
if org_model is None:
raise OrgNotFoundException(org_id)
return org_model
org_model_query_dependency = Annotated[type[Org], Depends(get_org_model_query)]
org_model_query_dependency = Annotated[Org, Depends(get_org_model_query)]
def get_org_model_body(db: db_dependency, request_model: OrgIDMixin) -> type[Org]:
def get_org_model_body(db: db_dependency, request_model: OrgIDMixin) -> Org:
org_id: Optional[int] = getattr(request_model, "organisation_id", None)
if org_id is None:
raise OrgNotFoundException()
@ -41,4 +39,4 @@ def get_org_model_body(db: db_dependency, request_model: OrgIDMixin) -> type[Org
return org_model
org_model_body_dependency = Annotated[type[Org], Depends(get_org_model_body)]
org_model_body_dependency = Annotated[Org, Depends(get_org_model_body)]

View file

@ -13,6 +13,7 @@ Models:
- owner_contact_rel: ORM relationship to Contact with owner_contact FK
- OrgUsers: org_id[FK][PK], user_id[FK][PK]
"""
from typing import Any
from sqlalchemy import ForeignKey
@ -30,15 +31,17 @@ class Organisation(CustomBase):
intake_questionnaire: Mapped[dict[str, Any] | None]
root_user_id: Mapped[int] = mapped_column(ForeignKey("user.id"))
billing_contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
security_contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
owner_contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"))
user_rel = relationship(
"User", secondary="orgusers", back_populates="organisation_rel"
billing_contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=True)
security_contact_id: Mapped[int] = mapped_column(
ForeignKey("contact.id"), nullable=True
)
owner_contact_id: Mapped[int] = mapped_column(ForeignKey("contact.id"), nullable=True)
group_rel = relationship("Group", back_populates="org_rel")
user_rel = relationship("User", secondary="orgusers", back_populates="organisation_rel")
group_rel = relationship(
"Group", back_populates="org_rel", cascade="all, delete-orphan"
)
root_user_rel = relationship("User", foreign_keys="Organisation.root_user_id")
billing_contact_rel = relationship(
@ -56,8 +59,9 @@ class Organisation(CustomBase):
)
@property
def root_user_email(self):
return self.root_user_rel.email if self.root_user_rel else None
def root_user_email(self) -> str:
return self.root_user_rel.email if self.root_user_rel else ""
class OrgUsers(CustomBase):
__tablename__ = "orgusers"
@ -65,4 +69,6 @@ class OrgUsers(CustomBase):
org_id: Mapped[int] = mapped_column(
ForeignKey("organisation.id", ondelete="CASCADE"), primary_key=True
)
user_id: Mapped[int] = mapped_column(ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
user_id: Mapped[int] = mapped_column(
ForeignKey("user.id", ondelete="CASCADE"), primary_key=True
)

View file

@ -133,9 +133,7 @@ async def get_org_by_id(
response_model=OrgPostOrgResponse,
responses={
status.HTTP_201_CREATED: {"description": "Successfully created organisation."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {
"description": "User must be logged in with OIDC to create organisation."
},
@ -169,6 +167,7 @@ async def create_org(
org_model = Org(
name=request_model.name,
intake_questionnaire=intake_questionnaire.model_dump(mode="json"),
root_user_id=user_model.id,
)
org_model.status = "partial"
@ -181,13 +180,10 @@ async def create_org(
isinstance(e.orig, UniqueViolation) # Postgres unique violation
or "UNIQUE constraint failed" in str(e.orig) # SQLite unique violation
):
raise ConflictException(
message="Organisation with this name already exists"
)
raise ConflictException(message="Organisation with this name already exists")
raise
# Adds currently logged-in user to org users list and sets them as root_user
org_model.user_rel.append(user_model)
org_model.root_user_rel = user_model
background_tasks.add_task(
assign_defaults, db, org_id=org_model.id, user_id=user_model.id
@ -214,9 +210,7 @@ async def create_org(
response_model=OrgPatchQuestionnaireResponse,
responses={
status.HTTP_200_OK: {"description": "Successfully updated questionnaire."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be org root user."
},
@ -234,12 +228,22 @@ async def update_questionnaire(
"""
org_status = StatusEnum(org_model.status)
if not org_status.is_pre_submission:
raise ForbiddenException(
"Questionnaire may only be modified prior to submission."
)
update_data = request_model.intake_questionnaire.model_dump(exclude_none=True)
raise ForbiddenException("Questionnaire may only be modified prior to submission.")
update_data: dict = request_model.intake_questionnaire.model_dump(exclude_none=True)
questionnaire = org_model.intake_questionnaire
questions_model = QuestionnaireQuestionsVersion0(**questionnaire["questions"])
if questionnaire is None:
questionnaire_questions = QuestionnaireQuestionsVersion0().model_dump()
questionnaire_metadata = QuestionnaireMetadata(version=0, submission_date=None)
questionnaire = Questionnaire(
metadata=questionnaire_metadata,
questions=questionnaire_questions,
).model_dump()
questions_model = QuestionnaireQuestionsVersion0(**questionnaire["questions"])
else:
questions_model = QuestionnaireQuestionsVersion0(**questionnaire["questions"])
for key, value in update_data.items():
if hasattr(questions_model, key):
setattr(questions_model, key, value)
@ -271,15 +275,9 @@ async def update_questionnaire(
status_code=status.HTTP_200_OK,
response_model=OrgPatchStatusResponse,
responses={
status.HTTP_200_OK: {
"description": "Successfully updated organisation status."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be super admin."
},
status.HTTP_200_OK: {"description": "Successfully updated organisation status."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_403_FORBIDDEN: {"description": "Not authorised. Must be super admin."},
},
)
async def update_status(
@ -329,15 +327,11 @@ async def get_users(org_model: org_model_root_claim_query_dependency):
status_code=status.HTTP_200_OK,
response_model=OrgPostUserResponse,
responses={
status.HTTP_200_OK: {
"description": "Successfully added user to the organisation."
},
status.HTTP_200_OK: {"description": "Successfully added user to the organisation."},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be org root user."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_409_CONFLICT: {
"description": "User is already a member of the organisation."
},
@ -378,12 +372,8 @@ async def add_user_to_org(
summary="Delete organisation from the hub.",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {
"description": "Successfully deleted organisation."
},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be super admin."
},
status.HTTP_204_NO_CONTENT: {"description": "Successfully deleted organisation."},
status.HTTP_403_FORBIDDEN: {"description": "Not authorised. Must be super admin."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Org ID missing or invalid."
},
@ -406,9 +396,7 @@ async def delete_organisation_by_id(
summary="Delete organisation from the hub as root user before it has been approved.",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {
"description": "Successfully deleted organisation."
},
status.HTTP_204_NO_CONTENT: {"description": "Successfully deleted organisation."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Unprocessable content.",
"content": {
@ -452,9 +440,7 @@ async def delete_organisation_by_id(
"content": {
"application/json": {
"examples": {
"db_id": {
"summary": "User not found in db when checking claims."
},
"db_id": {"summary": "User not found in db when checking claims."},
"user_model": {"summary": "User model not found in db."},
"org_model": {"summary": "Org model not found in db."},
}
@ -472,9 +458,7 @@ async def delete_preapproved_organisation_by_id(
"""
org_status = StatusEnum(org_model.status)
if not org_status.is_pre_approval:
raise ForbiddenException(
message="Organisation is no longer in pre-approval state."
)
raise ForbiddenException(message="Organisation is no longer in pre-approval state.")
db.delete(org_model)
db.commit()
@ -487,9 +471,7 @@ async def delete_preapproved_organisation_by_id(
response_model=OrgPatchRootResponse,
responses={
status.HTTP_200_OK: {"description": "Successfully updated root user."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_401_UNAUTHORIZED: {
"description": "Not authorised. Must be super admin."
},
@ -539,9 +521,7 @@ async def get_org_groups(org_model: org_model_root_claim_query_dependency):
"""
return {
"organisation": org_model,
"groups": [
{"id": group.id, "name": group.name} for group in org_model.group_rel
],
"groups": [{"id": group.id, "name": group.name} for group in org_model.group_rel],
}
@ -554,9 +534,7 @@ async def get_org_groups(org_model: org_model_root_claim_query_dependency):
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be org root user."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
},
)
async def remove_user_from_org(
@ -581,9 +559,7 @@ async def remove_user_from_org(
response_model=OrgGetContactResponse,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval of contact."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be org root user."
},
@ -626,9 +602,7 @@ async def get_contact(
response_model=OrgPatchContactResponse,
responses={
status.HTTP_200_OK: {"description": "Successfully updated contact."},
status.HTTP_422_UNPROCESSABLE_CONTENT: {
"description": "Invalid data in request."
},
status.HTTP_422_UNPROCESSABLE_CONTENT: {"description": "Invalid data in request."},
status.HTTP_403_FORBIDDEN: {
"description": "Not authorised. Must be org root user."
},

View file

@ -3,7 +3,6 @@ Reusable business logic functions for the organisation module
"""
from sqlalchemy.orm import Session
from typing import cast
from src.iam.service import assign_default_group
from src.organisation.models import Organisation as Org
@ -50,9 +49,6 @@ async def assign_defaults(
print("User not found while adding defaults")
return
org_model = cast(Org, org_model)
user_model = cast(User, user_model)
await add_default_org_permissions(db, org_model, default_org_permissions)
await assign_default_group(
db=db,