From a80767d870807b362c782d3face6e9e74fea5fbc Mon Sep 17 00:00:00 2001 From: luxferre Date: Mon, 25 May 2026 12:06:24 +0100 Subject: [PATCH] feat: condensed user get endpoints The process also added improved ORM relationships for multiple models. --- src/iam/models.py | 9 ++++++ src/organisation/models.py | 6 ++-- src/user/models.py | 24 +++++++++++++-- src/user/router.py | 62 +------------------------------------- src/user/schemas.py | 3 ++ 5 files changed, 39 insertions(+), 65 deletions(-) diff --git a/src/iam/models.py b/src/iam/models.py index 5ec6065..6e9ca70 100644 --- a/src/iam/models.py +++ b/src/iam/models.py @@ -6,6 +6,7 @@ Models: - Models: Description """ from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.orm import relationship from src.database import Base @@ -29,6 +30,14 @@ class Group(Base): org_id = Column(Integer, ForeignKey("organisation.id", ondelete="CASCADE")) + user_rel = relationship( + "User", + secondary="user_groups", + back_populates="group_rel" + ) + + org_rel = relationship("Organisation", back_populates="group_rel") + class GroupPermissions(Base): __tablename__ = "group_permissions" diff --git a/src/organisation/models.py b/src/organisation/models.py index ceae8f7..c85844a 100644 --- a/src/organisation/models.py +++ b/src/organisation/models.py @@ -26,12 +26,14 @@ class Organisation(Base): security_contact_id = Column(Integer, ForeignKey("contact.id")) owner_contact_id = Column(Integer, ForeignKey("contact.id")) - users = relationship( + user_rel = relationship( "User", secondary="orgusers", - back_populates="organisations" + back_populates="organisation_rel" ) + group_rel = relationship("Group", back_populates="org_rel") + class OrgUsers(Base): __tablename__ = "orgusers" diff --git a/src/user/models.py b/src/user/models.py index 7bce630..eac4684 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -4,10 +4,13 @@ Database models for user module Models: - User - id[pk], email, first_name, last_name, oidc_id """ +from collections import defaultdict + from sqlalchemy import Column, Integer, String from sqlalchemy.orm import relationship from src.database import Base +from src.iam.models import Group class User(Base): @@ -19,8 +22,25 @@ class User(Base): last_name = Column(String) oidc_id = Column(String, index=True, unique=True) - organisations = relationship( + organisation_rel = relationship( "Organisation", secondary="orgusers", - back_populates="users" + back_populates="user_rel" ) + + @property + def organisations(self): + return [org.name for org in self.organisation_rel] + + group_rel = relationship( + "Group", + secondary="user_groups", + back_populates="user_rel" + ) + + @property + def groups(self): + result = defaultdict(list) + for group in self.group_rel: + result[group.org_rel.name].append(group.name) + return dict(result) diff --git a/src/user/router.py b/src/user/router.py index 90e63ac..6d7a5a8 100644 --- a/src/user/router.py +++ b/src/user/router.py @@ -15,15 +15,12 @@ from typing import Annotated from fastapi import APIRouter from fastapi.params import Path -from sqlalchemy.sql import exists from starlette import status from src.user.models import User -from src.user.schemas import UserResponse, OrgResponse, OIDCClaims +from src.user.schemas import UserResponse, OIDCClaims from src.user.exceptions import UserNotFoundException -from src.organisation.models import OrgUsers, Organisation -from src.iam.models import Group, UserGroups from src.auth.service import claims_dependency from src.database import db_dependency @@ -63,30 +60,6 @@ async def current_user(user: claims_dependency, db: db_dependency): return user_model -@router.get("/self/orgs", response_model=list[OrgResponse], status_code=status.HTTP_200_OK, responses={ - status.HTTP_404_NOT_FOUND: {"description": "User not found"}, - status.HTTP_200_OK: {"description": "Successful retrieval from database"}, -}) -async def get_current_organisations(db: db_dependency, user: claims_dependency): - """ - Returns all organisations associated with the currently logged-in user. - """ - user_id = user.get("db_id", None) - if user_id is None: - raise UserNotFoundException() - user_exists = db.query(exists().where(User.id == user_id)).scalar() - if not user_exists: - raise UserNotFoundException(user_id=user_id) - - org_user_models = (db.query(OrgUsers.org_id, Organisation.name) - .join(OrgUsers, Organisation.id == OrgUsers.org_id) - .filter(OrgUsers.user_id == user_id) - .all() - ) - - return org_user_models - - @router.get("/{user_id}", response_model=UserResponse, status_code=status.HTTP_200_OK, responses={ status.HTTP_404_NOT_FOUND: {"description": "User not found"}, status.HTTP_200_OK: {"description": "Successful retrieval from database"}, @@ -102,27 +75,6 @@ async def get_user_by_id(db: db_dependency, user_id: Annotated[int, Path(gt=0,de return user_model -@router.get("/{user_id}/orgs", response_model=list[OrgResponse], status_code=status.HTTP_200_OK, responses={ - status.HTTP_404_NOT_FOUND: {"description": "User not found"}, - status.HTTP_200_OK: {"description": "Successful retrieval from database"}, -}) -async def get_organisations(db: db_dependency, user_id: Annotated[int, Path(gt=0,description="User database ID")]): - """ - Returns all organisations associated with the provided user ID. - """ - user_exists = db.query(exists().where(User.id == user_id)).scalar() - if not user_exists: - raise UserNotFoundException(user_id=user_id) - - org_user_models = (db.query(OrgUsers.org_id, Organisation.name) - .join(OrgUsers, Organisation.id == OrgUsers.org_id) - .filter(OrgUsers.user_id == user_id) - .all() - ) - - return org_user_models - - @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, responses={ status.HTTP_204_NO_CONTENT: {"description": "User deleted"}, status.HTTP_404_NOT_FOUND: {"description": "User not found"}, @@ -136,15 +88,3 @@ async def delete_user_by_id(user_id: Annotated[int, Path(gt=0)], db: db_dependen raise UserNotFoundException(user_id=user_id) db.delete(user_model) db.commit() - - -@router.get("/{user_id}/groups") -async def get_user_groups(db: db_dependency, user_id: Annotated[int, Path(gt=0,description="User database ID")]): - user_model = (db.query(User).filter(User.id == user_id).first()) - if user_model is None: - raise UserNotFoundException(user_id=user_id) - - user_groups = db.query(Group).join(UserGroups).filter(UserGroups.user_id==user_id).all() - - # TODO: Response model - return user_groups diff --git a/src/user/schemas.py b/src/user/schemas.py index f33ae54..7e3dc12 100644 --- a/src/user/schemas.py +++ b/src/user/schemas.py @@ -5,6 +5,7 @@ Models: - List: Description - Models: Description """ +from typing import Optional from src.schemas import CustomBaseModel @@ -44,6 +45,8 @@ class UserResponse(CustomBaseModel): first_name: str last_name: str email: str + organisations: list[Optional[str]] + groups: dict[str, list[str]] class OrgResponse(CustomBaseModel):