From 2345742dd7e3270d342abd44af8b5f50b6682ad5 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Thu, 27 Jun 2024 10:58:41 +0100 Subject: [PATCH] Added API endpoint to retrieve users for fund --- core/account.py | 31 +++++++++++++ openapi/api.yml | 44 ++++++++++++++++++ tests/conftest.py | 15 ++++++ tests/test_accounts.py | 102 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+) diff --git a/core/account.py b/core/account.py index cf2e2dd4..86f7bcd0 100644 --- a/core/account.py +++ b/core/account.py @@ -10,6 +10,7 @@ from sqlalchemy import delete from sqlalchemy import or_ from sqlalchemy import select +from sqlalchemy.orm import selectinload from db import db from db.models.account import Account @@ -207,3 +208,33 @@ def post_account() -> Tuple[dict, int]: "An account with that email or azure_ad_subject_id already exists", 409, ) + + +def get_accounts_for_fund(fund_short_name): + include_assessors = True if request.args.get("include_assessors", "true").lower() == "true" else False + include_commenters = True if request.args.get("include_commenters", "true").lower() == "true" else False + round_short_name = request.args.get("round_short_name") + if not include_assessors and not include_commenters: + return {"error": "One of include_assessors or include_commenters must be true"}, 400 + query = ( + db.session.query(Account) + .join(Role) # Explicitly join the tables + .filter(Role.role.like(f"%{fund_short_name}%")) # Filter based on the Role attribute + .options(selectinload(Account.roles)) + ) + if round_short_name: + query = query.filter(Role.role.like(f"%{round_short_name}%")) + + if include_commenters and not include_assessors: + query = query.filter(Role.role.like("%COMMENTER%")) + elif include_assessors and not include_commenters: + query = query.filter(Role.role.like("%ASSESSOR%")) + else: + query = query.filter(or_(Role.role.like("%ASSESSOR%"), Role.role.like("%COMMENTER%"))) + + results = query.all() + if not results: + return {"error": "No matching accounts found"}, 404 + + account_schema = AccountSchema() + return account_schema.dump(results, many=True), 200 diff --git a/openapi/api.yml b/openapi/api.yml index efe89423..85f216e1 100644 --- a/openapi/api.yml +++ b/openapi/api.yml @@ -105,6 +105,50 @@ paths: schema: type: string format: path + /accounts/fund/{fund_short_name}: + get: + tags: + - accounts + summary: Return the users assigned roles related to the fund + description: "Given a fund, return the users assigned with a role. Filterable by assessors, commenters and round" + operationId: core.account.get_accounts_for_fund + parameters: + - name: fund_short_name + in: path + schema: + type: string + required: true + - name: include_assessors + in: query + description: "Results will include assessors" + required: false + schema: + type: string + default: "true" + - name: include_commenters + in: query + description: "Results will include commenters" + required: false + schema: + type: string + default: "true" + - name: round_short_name + in: query + description: "Results will include only roles for that round" + required: false + schema: + type: string + example: "R1" + responses: + 200: + description: One or more account exist and are associated with the fund and round + content: + application/json: + schema: + type: array + $ref: '#/components/schemas/account' + 404: + description: "No associated accounts found." /bulk-accounts: get: tags: diff --git a/tests/conftest.py b/tests/conftest.py index 8a5c2631..4e577e29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,3 +80,18 @@ def seed_test_data(request, app, clear_test_data, _db): for user in users_to_create: create_user_with_roles(user, _db) yield users_to_create + + +@pytest.fixture(scope="function") +def seed_test_data_fn(request, app, clear_test_data, _db): + marker = request.node.get_closest_marker("user_config") + if not marker: + users_to_create = [test_user_1, test_user_2, test_user_to_update] + else: + users_to_create = marker.args[0] + for user in users_to_create: + create_user_with_roles(user, _db) + yield users_to_create + Role.query.delete() + Account.query.delete() + _db.session.commit() diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 9c171088..e3c90762 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -2,6 +2,8 @@ Tests the GET and POST functionality of our api. """ +import uuid + import pytest from tests.conftest import test_user_1 @@ -327,3 +329,103 @@ def test_update_role_with_non_existent_role_allows(self, flask_test_client, clea response = flask_test_client.put(url, json=params) assert response.status_code == 201 + + +class TestGetAccountsForFund: + @pytest.mark.user_config( + [ + { + "email": "assessor@example.com", + "subject_id": "1", + "account_id": uuid.uuid4(), + "roles": ["COF_ASSESSOR_R1"], + }, + { + "email": "commenter@example.com", + "subject_id": "2", + "account_id": uuid.uuid4(), + "roles": ["COF_COMMENTER_R1"], + }, + { + "email": "otherround@example.com", + "subject_id": "3", + "account_id": uuid.uuid4(), + "roles": ["COF_COMMENTER_R2"], + }, + { + "email": "otherfund@example.com", + "subject_id": "4", + "account_id": uuid.uuid4(), + "roles": ["HSRA_ASSESSOR_R1"], + }, + ] + ) + def test_successful_retrieval(self, flask_test_client, seed_test_data_fn): + response = flask_test_client.get("/accounts/fund/COF") + assert response.status_code == 200 + assert len(response.json) == 3 + + @pytest.mark.user_config( + [ + { + "email": "assessor@example.com", + "subject_id": "1", + "account_id": uuid.uuid4(), + "roles": ["COF_ASSESSOR_R1"], + }, + { + "email": "commenter@example.com", + "subject_id": "2", + "account_id": uuid.uuid4(), + "roles": ["COF_COMMENTER_R1"], + }, + ] + ) + def test_assessors_only(self, flask_test_client, seed_test_data_fn): + response = flask_test_client.get("/accounts/fund/COF?include_assessors=true&include_commenters=false") + assert response.status_code == 200 + assert len(response.json) == 1 # Only the assessor should be returned + assert all("ASSESSOR" in role for role in response.json[0]["roles"]) + + @pytest.mark.user_config( + [ + { + "email": "assessor@example.com", + "subject_id": "1", + "account_id": uuid.uuid4(), + "roles": ["COF_ASSESSOR_R1"], + }, + { + "email": "commenter@example.com", + "subject_id": "2", + "account_id": uuid.uuid4(), + "roles": ["COF_COMMENTER_R1"], + }, + ] + ) + def test_commenters_only(self, flask_test_client, seed_test_data_fn): + response = flask_test_client.get("/accounts/fund/COF?include_assessors=false&include_commenters=true") + assert response.status_code == 200 + assert len(response.json) == 1 # Only the commenter should be returned + assert all("COMMENTER" in role for role in response.json[0]["roles"]) + + @pytest.mark.user_config([]) # No users configured + def test_no_matching_accounts(self, flask_test_client, seed_test_data_fn): + response = flask_test_client.get("/accounts/fund/unknownfund") + assert response.status_code == 404 + assert response.json == {"error": "No matching accounts found"} + + @pytest.mark.user_config( + [ + { + "email": "both@example.com", + "subject_id": "3", + "account_id": uuid.uuid4(), + "roles": ["COF_ASSESSOR_R1", "COF_COMMENTER_R1"], + } + ] + ) + def test_bad_request(self, flask_test_client, seed_test_data_fn): + response = flask_test_client.get("/accounts/fund/COF?include_assessors=false&include_commenters=false") + assert response.status_code == 400 + assert response.json == {"error": "One of include_assessors or include_commenters must be true"}