diff --git a/modules/features/trustee/interfaceFeatureTrustee.py b/modules/features/trustee/interfaceFeatureTrustee.py index 99553108..b5e08e2b 100644 --- a/modules/features/trustee/interfaceFeatureTrustee.py +++ b/modules/features/trustee/interfaceFeatureTrustee.py @@ -1095,7 +1095,8 @@ class TrusteeObjects: def deleteDocument(self, documentId: str) -> bool: """Delete a document. - Note: organisationId and contractId removed - feature instance IS the organisation. + All position-document cross-table entries (TrusteePositionDocument) referencing + this document are deleted first, then the document. """ # Get existing document to check creator existingRecords = self.db.getRecordset(TrusteeDocument, recordFilter={"id": documentId}) @@ -1112,6 +1113,7 @@ class TrusteeObjects: logger.warning(f"User {self.userId} lacks permission to delete document") return False + self._deletePositionDocumentLinksForDocument(documentId) return self.db.recordDelete(TrusteeDocument, documentId) # ===== Position CRUD ===== @@ -1259,7 +1261,8 @@ class TrusteeObjects: def deletePosition(self, positionId: str) -> bool: """Delete a position. - Note: organisationId and contractId removed - feature instance IS the organisation. + All position-document cross-table entries (TrusteePositionDocument) referencing + this position are deleted first, then the position. """ # Get existing position to check creator existingRecords = self.db.getRecordset(TrusteePosition, recordFilter={"id": positionId}) @@ -1276,6 +1279,7 @@ class TrusteeObjects: logger.warning(f"User {self.userId} lacks permission to delete position") return False + self._deletePositionDocumentLinksForPosition(positionId) return self.db.recordDelete(TrusteePosition, positionId) # ===== Position-Document Link CRUD ===== @@ -1423,6 +1427,22 @@ class TrusteeObjects: return self.db.recordDelete(TrusteePositionDocument, linkId) + def _deletePositionDocumentLinksForDocument(self, documentId: str) -> None: + """Delete all position-document cross-table entries referencing this document.""" + links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"documentId": documentId}) + for link in links: + linkId = link.get("id") + if linkId: + self.db.recordDelete(TrusteePositionDocument, linkId) + + def _deletePositionDocumentLinksForPosition(self, positionId: str) -> None: + """Delete all position-document cross-table entries referencing this position.""" + links = self.db.getRecordset(TrusteePositionDocument, recordFilter={"positionId": positionId}) + for link in links: + linkId = link.get("id") + if linkId: + self.db.recordDelete(TrusteePositionDocument, linkId) + # ===== Trustee-specific Access Check ===== def getUserAccessForOrganisation(self, userId: str, organisationId: str) -> List[Dict[str, Any]]: diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py index 82b796c1..56a79741 100644 --- a/modules/routes/routeAdminFeatures.py +++ b/modules/routes/routeAdminFeatures.py @@ -878,6 +878,12 @@ class FeatureInstanceUserResponse(BaseModel): enabled: bool +class FeatureInstanceUserUpdate(BaseModel): + """Request model for updating a feature instance user (roles and active flag)""" + roleIds: List[str] = Field(..., description="Role IDs to assign") + enabled: Optional[bool] = Field(None, description="Whether this user's access is active (omit to leave unchanged)") + + @router.get("/instances/{instanceId}/users", response_model=List[FeatureInstanceUserResponse]) @limiter.limit("60/minute") async def list_feature_instance_users( @@ -1161,18 +1167,19 @@ async def update_feature_instance_user_roles( request: Request, instanceId: str, userId: str, - roleIds: List[str], + data: FeatureInstanceUserUpdate, context: RequestContext = Depends(getRequestContext) ) -> Dict[str, Any]: """ - Update a user's roles in a feature instance. + Update a user's roles and active flag in a feature instance. Replaces all existing FeatureAccessRole records with new ones. + If enabled is provided, updates the FeatureAccess.enabled flag. Args: instanceId: FeatureInstance ID userId: User ID to update - roleIds: New list of role IDs + data: roleIds and optional enabled """ try: rootInterface = getRootInterface() @@ -1215,6 +1222,10 @@ async def update_feature_instance_user_roles( featureAccessId = existingAccess[0].get("id") + # Update enabled flag if provided + if data.enabled is not None: + rootInterface.db.recordModify(FeatureAccess, featureAccessId, {"enabled": data.enabled}) + # Delete existing FeatureAccessRole records existingRoles = rootInterface.db.getRecordset( FeatureAccessRole, @@ -1224,7 +1235,7 @@ async def update_feature_instance_user_roles( rootInterface.db.recordDelete(FeatureAccessRole, role.get("id")) # Create new FeatureAccessRole records - for roleId in roleIds: + for roleId in data.roleIds: featureAccessRole = FeatureAccessRole( featureAccessId=featureAccessId, roleId=roleId @@ -1232,14 +1243,15 @@ async def update_feature_instance_user_roles( rootInterface.db.recordCreate(FeatureAccessRole, featureAccessRole.model_dump()) logger.info( - f"User {context.user.id} updated roles for user {userId} in feature instance {instanceId}: {roleIds}" + f"User {context.user.id} updated roles for user {userId} in feature instance {instanceId}: {data.roleIds}" ) return { "featureAccessId": featureAccessId, "userId": userId, "featureInstanceId": instanceId, - "roleIds": roleIds + "roleIds": data.roleIds, + "enabled": data.enabled if data.enabled is not None else existingAccess[0].get("enabled", True) } except HTTPException: