From b1dd59a68b17d8900ad8df8d62752d4b4c2fbf5d Mon Sep 17 00:00:00 2001 From: Odysseus Chiu Date: Wed, 4 Dec 2024 22:54:11 -0800 Subject: [PATCH 1/3] 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"] From fe394a515cca671fed9e950bc535ac30b1499ccf Mon Sep 17 00:00:00 2001 From: Odysseus Chiu Date: Wed, 4 Dec 2024 23:01:10 -0800 Subject: [PATCH 2/3] lint --- auth-api/src/auth_api/models/org.py | 6 ++++-- auth-api/tests/unit/api/test_org.py | 32 ++++++++++++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/auth-api/src/auth_api/models/org.py b/auth-api/src/auth_api/models/org.py index debb152ee5..70b756a1e2 100644 --- a/auth-api/src/auth_api/models/org.py +++ b/auth-api/src/auth_api/models/org.py @@ -172,7 +172,8 @@ def search_org(cls, search: OrgSearch, environment: str): if search.name: query = query.filter(Org.name.ilike(f"%{search.name}%")) if search.member_search_text: - member_exists_subquery = text(""" + member_exists_subquery = text( + """ EXISTS ( SELECT 1 FROM memberships @@ -182,7 +183,8 @@ def search_org(cls, search: OrgSearch, environment: str): 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}%") + """ + ).params(member_search_text=f"%{search.member_search_text}%") query = query.filter(member_exists_subquery) diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index 55c4e32174..8aa17a25c3 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -2942,37 +2942,41 @@ def test_search_org_members(client, jwt, session, keycloak_mock): # pylint:disa 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") + 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'] + 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']}', + f"/api/v1/orgs?status=ACTIVE&includeMembers=true&members={user_info['lastname']} {user_info['firstname']}", headers=headers, - content_type="application/json") + 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'] + 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', + f"/api/v1/orgs?status=ACTIVE&includeMembers=true&members=NOTHING", headers=headers, - content_type="application/json") + content_type="application/json", + ) dictionary = json.loads(rv.data) assert not dictionary["orgs"] From de411b75e845c52d8a67682f26022318a175d9eb Mon Sep 17 00:00:00 2001 From: Odysseus Chiu Date: Wed, 4 Dec 2024 23:04:10 -0800 Subject: [PATCH 3/3] more linting --- auth-api/tests/unit/api/test_org.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth-api/tests/unit/api/test_org.py b/auth-api/tests/unit/api/test_org.py index 8aa17a25c3..398a382f37 100644 --- a/auth-api/tests/unit/api/test_org.py +++ b/auth-api/tests/unit/api/test_org.py @@ -2974,7 +2974,7 @@ def test_search_org_members(client, jwt, session, keycloak_mock): # pylint:disa assert user["lastname"] == user_info["lastname"] rv = client.get( - f"/api/v1/orgs?status=ACTIVE&includeMembers=true&members=NOTHING", + "/api/v1/orgs?status=ACTIVE&includeMembers=true&members=NOTHING", headers=headers, content_type="application/json", )