From b1dd59a68b17d8900ad8df8d62752d4b4c2fbf5d Mon Sep 17 00:00:00 2001 From: Odysseus Chiu Date: Wed, 4 Dec 2024 22:54:11 -0800 Subject: [PATCH] 21429 - staff account search for members --- auth-api/src/auth_api/models/dataclass.py | 2 ++ auth-api/src/auth_api/models/org.py | 14 ++++++++ auth-api/src/auth_api/resources/v1/org.py | 2 ++ auth-api/src/auth_api/services/org.py | 7 +++- auth-api/tests/unit/api/test_org.py | 44 +++++++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) 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..debb152ee5 100644 --- a/auth-api/src/auth_api/models/org.py +++ b/auth-api/src/auth_api/models/org.py @@ -171,6 +171,20 @@ 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..55c4e32174 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -2932,3 +2932,47 @@ 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( + f'/api/v1/orgs?status=ACTIVE&includeMembers=true&members=NOTHING', + headers=headers, + content_type="application/json") + dictionary = json.loads(rv.data) + assert not dictionary["orgs"]