diff --git a/auth-api/src/auth_api/models/dataclass.py b/auth-api/src/auth_api/models/dataclass.py index 3dfddabca3..ee68dd1940 100644 --- a/auth-api/src/auth_api/models/dataclass.py +++ b/auth-api/src/auth_api/models/dataclass.py @@ -118,6 +118,8 @@ class OrgSearch: # pylint: disable=too-many-instance-attributes id: str decision_made_by: str org_type: str + include_members: bool + member_search_text: str page: int limit: int diff --git a/auth-api/src/auth_api/models/org.py b/auth-api/src/auth_api/models/org.py index ba9c7f7269..70b756a1e2 100644 --- a/auth-api/src/auth_api/models/org.py +++ b/auth-api/src/auth_api/models/org.py @@ -171,6 +171,22 @@ def search_org(cls, search: OrgSearch, environment: str): query = query.filter(Org.branch_name.ilike(f"%{search.branch_name}%")) if search.name: query = query.filter(Org.name.ilike(f"%{search.name}%")) + if search.member_search_text: + member_exists_subquery = text( + """ + EXISTS ( + SELECT 1 + FROM memberships + JOIN users ON users.id = memberships.user_id + WHERE memberships.org_id = orgs.id + AND memberships.status = 1 + AND users.status = 1 + AND CONCAT(users.last_name, ' ', users.first_name, ' ', users.username) ILIKE :member_search_text + ) + """ + ).params(member_search_text=f"%{search.member_search_text}%") + + query = query.filter(member_exists_subquery) query = cls._search_by_business_identifier(query, search.business_identifier, environment) query = cls._search_for_statuses(query, search.statuses) diff --git a/auth-api/src/auth_api/resources/v1/org.py b/auth-api/src/auth_api/resources/v1/org.py index 6fd30c0b6d..7192cf60b9 100644 --- a/auth-api/src/auth_api/resources/v1/org.py +++ b/auth-api/src/auth_api/resources/v1/org.py @@ -63,6 +63,8 @@ def search_organizations(): extract_numbers(request.args.get("id", None)), request.args.get("decisionMadeBy", None), request.args.get("orgType", None), + bool(request.args.get("includeMembers", False)), + request.args.get("members", None), int(request.args.get("page", 1)), int(request.args.get("limit", 10)), ) diff --git a/auth-api/src/auth_api/services/org.py b/auth-api/src/auth_api/services/org.py index 3d1af24a20..8576fb3b47 100644 --- a/auth-api/src/auth_api/services/org.py +++ b/auth-api/src/auth_api/services/org.py @@ -36,7 +36,7 @@ from auth_api.models.affidavit import Affidavit as AffidavitModel from auth_api.models.dataclass import Activity, DeleteAffiliationRequest from auth_api.models.org import OrgSearch -from auth_api.schemas import ContactSchema, InvitationSchema, OrgSchema +from auth_api.schemas import ContactSchema, InvitationSchema, MembershipSchema, OrgSchema from auth_api.services.user import User as UserService from auth_api.services.validators.access_type import validate as access_type_validate from auth_api.services.validators.account_limit import validate as account_limit_validate @@ -800,6 +800,11 @@ def search_orgs(search: OrgSearch, environment): # pylint: disable=too-many-loc if include_invitations and org.invitations else [] ), + "members": ( + MembershipSchema(exclude=("org", "user.contacts")).dump(org.members, many=True) + if search.include_members and org.members + else [] + ), } ) return orgs_result diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index 5962fe09b6..398a382f37 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -2932,3 +2932,51 @@ def test_update_org_api_access(client, jwt, session, keycloak_mock): # pylint:d assert rv.status_code == HTTPStatus.OK dictionary = json.loads(rv.data) assert dictionary["hasApiAccess"] is True + + +def test_search_org_members(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument + """Assert that a list of members for an org search can be retrieved.""" + user_info = TestJwtClaims.public_user_role + headers = factory_auth_header(jwt=jwt, claims=user_info) + client.post("/api/v1/users", headers=headers, content_type="application/json") + client.post("/api/v1/orgs", data=json.dumps(TestOrgInfo.org1), headers=headers, content_type="application/json") + + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_view_accounts_role) + rv = client.get( + f"/api/v1/orgs?status=ACTIVE&includeMembers=true&members={user_info['preferred_username']}", + headers=headers, + content_type="application/json", + ) + assert rv.status_code == HTTPStatus.OK + dictionary = json.loads(rv.data) + assert dictionary["orgs"] + assert len(dictionary["orgs"][0]["members"]) == 1 + member = dictionary["orgs"][0]["members"][0] + assert member["membershipTypeCode"] == "ADMIN" + assert member["user"] + user = member["user"] + assert user["username"] == user_info["preferred_username"] + + rv = client.get( + f"/api/v1/orgs?status=ACTIVE&includeMembers=true&members={user_info['lastname']} {user_info['firstname']}", + headers=headers, + content_type="application/json", + ) + + dictionary = json.loads(rv.data) + assert dictionary["orgs"] + assert len(dictionary["orgs"][0]["members"]) == 1 + member = dictionary["orgs"][0]["members"][0] + assert member["membershipTypeCode"] == "ADMIN" + assert member["user"] + user = member["user"] + assert user["firstname"] == user_info["firstname"] + assert user["lastname"] == user_info["lastname"] + + rv = client.get( + "/api/v1/orgs?status=ACTIVE&includeMembers=true&members=NOTHING", + headers=headers, + content_type="application/json", + ) + dictionary = json.loads(rv.data) + assert not dictionary["orgs"]