Skip to content

Commit ce9d751

Browse files
authored
Merge pull request #58 from IATI/sk-paging-on-reporting-org
Add paging to GET /reporting-orgs, and two small fixes.
2 parents b4e47ad + a7013d7 commit ce9d751

16 files changed

+802
-66
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ __pycache__/
1010

1111
.ve
1212
.venv
13+
14+
# Project-specific items
15+
/keys/
16+
/logs/
17+
18+
/.env
19+
/test.db
20+
/test-audit-log-public-key.pem

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919

2020
### Security
2121

22+
## [0.2.6]
23+
24+
### Added
25+
26+
- Added paging to GET /reporting-orgs, as per the specification.
27+
28+
### Fixed
29+
30+
- Fixed the 'last' paging link when there are no results to point to page 1.
31+
2232
## [0.2.5]
2333

2434
### Fixed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "register-your-data-api"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
requires-python = ">= 3.12.11"
55
readme = "README.md"
66
authors = [{name="IATI Secretariat", email="[email protected]"}]
@@ -12,7 +12,7 @@ dependencies = [
1212
"click",
1313
"cryptography==45.0.5",
1414
"fastapi[standard]==0.116.0",
15-
"libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.4.0",
15+
"libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.5.0",
1616
"prometheus-client>=0.22.1",
1717
"pyjwt>=2.10.0",
1818
"python-dotenv>=1.1.1",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ idna==3.10
6565
# requests
6666
jinja2==3.1.6
6767
# via fastapi
68-
libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.4.0
68+
libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.5.0
6969
# via register-your-data-api (pyproject.toml)
7070
mako==1.3.10
7171
# via alembic

requirements_dev.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ isort==6.0.1
8787
# via register-your-data-api (pyproject.toml)
8888
jinja2==3.1.6
8989
# via fastapi
90-
libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.4.0
90+
libsuitecrm @ git+https://github.com/iati/iati-registry-libsuitecrm@v0.5.0
9191
# via register-your-data-api (pyproject.toml)
9292
mako==1.3.10
9393
# via alembic

src/register_your_data_api/client_application_details_provider.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from logging import Logger
12
from typing import Annotated
23
from uuid import UUID
34

@@ -23,11 +24,17 @@ class ClientApplicationDetails(BaseModel):
2324

2425
class ClientApplicationDetailsProvider:
2526

27+
_app_logger: Logger
28+
_audit_logger: Logger
2629
_CLIENT_APPLICATION_DETAILS: dict[str, ClientApplicationDetails] = {}
2730

28-
def __init__(self, filename: str) -> None:
31+
def __init__(self, filename: str, app_logger: Logger, audit_logger: Logger) -> None:
2932
"""Initializes the provider by loading client application details from a JSON file."""
3033

34+
self._app_logger = app_logger
35+
36+
self._audit_logger = audit_logger
37+
3138
try:
3239
with open(filename, "r") as file:
3340
data = file.read()
@@ -46,6 +53,9 @@ def get_client_application_details(self, client_id: str) -> ClientApplicationDet
4653
"""Retrieves details about the client application given its client ID."""
4754

4855
if client_id not in self._CLIENT_APPLICATION_DETAILS:
49-
raise ValueError("Unknown client application")
56+
error_message = f"Unknown client application. Client id: {client_id} is not found in the list of clients"
57+
self._app_logger.error(error_message)
58+
self._audit_logger.error(error_message)
59+
raise ValueError(error_message)
5060

5161
return self._CLIENT_APPLICATION_DETAILS[client_id]

src/register_your_data_api/data_handling/data_schemas.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,3 @@ class UserReportingOrgRelationSingleResponse(pydantic.BaseModel):
231231
data: UserReportingOrgRelation
232232
error: str | None = pydantic.Field(None)
233233
status: str
234-
235-
236-
class UserReportingOrgRelationListResponse(pydantic.BaseModel):
237-
238-
data: list[UserReportingOrgRelation | UserReportingOrgDiscoverableMetadataRelation]
239-
error: str | None = pydantic.Field(None)
240-
status: str

src/register_your_data_api/response_schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def create(
2828
if page_size == 0:
2929
raise ValueError("page_size must be greater than 0")
3030

31-
total_pages = -(-total_records // page_size)
31+
total_pages = max(1, -(-total_records // page_size))
3232

3333
next_page_url = None
3434
if page < total_pages:

src/register_your_data_api/routers/reporting_orgs.py

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from fastapi import Depends, Security
99
from fastapi.exceptions import HTTPException
1010
from fastapi.responses import JSONResponse
11-
from libsuitecrm import Filter, SuiteCRM # type: ignore
11+
from libsuitecrm import Filter, RequestFailed, SuiteCRM # type: ignore
1212

1313
from register_your_data_api.dependencies import get_suitecrm_audit_headers
1414

@@ -37,7 +37,6 @@
3737
ReportingOrgUserCreateModel,
3838
UserReportingOrgDiscoverableMetadataRelation,
3939
UserReportingOrgRelation,
40-
UserReportingOrgRelationListResponse,
4140
UserReportingOrgRelationSingleResponse,
4241
)
4342
from ..response_schemas import PaginatedResultsPage
@@ -51,62 +50,80 @@
5150
def get_reporting_orgs(
5251
request: starlette.requests.Request,
5352
user: auth_models.UserAndCredentials = Security(authz.get_user_authnz, scopes=["ryd", "ryd:reporting_org"]),
53+
paging: PaginationQueryParams = fastapi.Depends(),
5454
include_actions: str = "no",
55-
) -> UserReportingOrgRelationListResponse:
55+
) -> PaginatedResultsPage[UserReportingOrgRelation | UserReportingOrgDiscoverableMetadataRelation]:
5656

5757
context: Context = request.app.state.context
5858

59-
user_reporting_org_associations = user.validator.get_users_fine_grained_associations()
59+
crm: SuiteCRM = context.suitecrm_client_factory.get_client()
60+
61+
try:
62+
orgs_for_user = crm.get_relationship(
63+
"Contacts",
64+
user.user_id_crm,
65+
"Accounts",
66+
page_number=paging.page,
67+
page_size=paging.page_size,
68+
sort_field="name",
69+
sort_dir="ascending",
70+
filters=Filter().equal("iati_registry_discoverable", "1"),
71+
)
72+
except RequestFailed as e:
73+
error_id = uuid.uuid4()
74+
public_error_message = (
75+
"There was a problem fetching the list of reporting orgs you are associated with. "
76+
f"Please try again later, or contact IATI Support quoting error id: {error_id}"
77+
)
78+
context.app_logger.error(
79+
f"Error: error id: {error_id} - user id: {user.user_id_crm} - GET /reporting-orgs - Problem when fetching "
80+
"the list of reporting organisations for this user from SuiteCRM. "
81+
f"Details: {str(e)}"
82+
)
83+
raise HTTPException(status_code=fastapi.status.HTTP_500_INTERNAL_SERVER_ERROR, detail=public_error_message)
84+
85+
total_records = orgs_for_user.get("meta", {}).get("total-records", 0)
6086

6187
reporting_orgs_list: list[UserReportingOrgRelation | UserReportingOrgDiscoverableMetadataRelation] = []
6288

63-
if len(user_reporting_org_associations) > 0:
89+
for reporting_org_from_suitecrm in orgs_for_user["data"]:
90+
role_for_org = user.validator.get_user_role_for_reporting_org(reporting_org_from_suitecrm["id"])
6491

65-
crm: SuiteCRM = context.suitecrm_client_factory.get_client()
92+
if role_for_org is None:
93+
context.app_logger.info(
94+
f"Info: user id: {user.user_id_crm} - GET /reporting-orgs - user is associated with organisation "
95+
f"{reporting_org_from_suitecrm["id"]} in the CRM but has no role for that organisation in the FGA DB. "
96+
"Organisation was omitted from the list returned to the user."
97+
)
98+
continue
6699

67-
fields = SUITECRM_REPORTING_ORG_FIELDS
100+
reporting_org_obj: ReportingOrgMetadata | DiscoverableReportingOrgMetadata
68101

69-
# The OR search in SuiteCRM appears to be broken; you can't search for items where id = 'A' OR id = 'B' OR ...
70-
# It appears this doesn't work when the field being searched on is the same in each case. So we have to fetch
71-
# the details for reporting orgs the user is associated with one at a time.
72-
suitecrm_collected_responses: list[dict[str, Any]] = []
73-
for user_reporting_org_association in user_reporting_org_associations:
74-
filters = Filter()
75-
filters.equal("id", str(user_reporting_org_association.reporting_org))
76-
filters.equal("iati_registry_discoverable", "1")
77-
crm_reporting_org = crm.get_records("Accounts", fields=fields, filters=filters)
78-
if "data" in crm_reporting_org and len(crm_reporting_org["data"]) > 0:
79-
suitecrm_collected_responses.append(*crm_reporting_org["data"])
80-
81-
for reporting_org_from_suitecrm in suitecrm_collected_responses:
82-
role_for_org = user.validator.get_user_role_for_reporting_org(reporting_org_from_suitecrm["id"])
83-
reporting_org_obj: ReportingOrgMetadata | DiscoverableReportingOrgMetadata
84-
85-
if role_for_org == fga_models.FineGrainedAuthorisationRole.CONTRIBUTOR_PENDING:
86-
reporting_org_obj = get_discoverable_reporting_org_meta_from_suitecrm_response(
87-
reporting_org_from_suitecrm["attributes"]
88-
)
89-
else:
90-
reporting_org_obj = get_reporting_org_meta_from_suitecrm_response(
91-
reporting_org_from_suitecrm["attributes"]
92-
)
102+
if role_for_org == fga_models.FineGrainedAuthorisationRole.CONTRIBUTOR_PENDING:
103+
reporting_org_obj = get_discoverable_reporting_org_meta_from_suitecrm_response(
104+
reporting_org_from_suitecrm["attributes"]
105+
)
106+
else:
107+
reporting_org_obj = get_reporting_org_meta_from_suitecrm_response(
108+
reporting_org_from_suitecrm["attributes"]
109+
)
93110

94-
reporting_orgs_list.append(
95-
UserReportingOrgRelation(
96-
id=reporting_org_from_suitecrm["id"],
97-
user_role=get_fga_role_as_str(role_for_org), # type: ignore
98-
metadata=reporting_org_obj,
99-
reporting_org_actions=(
100-
get_reporting_org_actions(crm, reporting_org_from_suitecrm["id"])
101-
if include_actions == "yes"
102-
else []
103-
),
104-
)
111+
reporting_orgs_list.append(
112+
UserReportingOrgRelation(
113+
id=reporting_org_from_suitecrm["id"],
114+
user_role=get_fga_role_as_str(role_for_org),
115+
metadata=reporting_org_obj,
116+
reporting_org_actions=(
117+
get_reporting_org_actions(crm, reporting_org_from_suitecrm["id"])
118+
if include_actions == "yes"
119+
else []
120+
),
105121
)
122+
)
106123

107-
reporting_orgs_list.sort(key=lambda org: org.metadata.human_readable_name.lower())
124+
reporting_orgs_list.sort(key=lambda org: org.metadata.human_readable_name.lower())
108125

109-
return UserReportingOrgRelationListResponse(status="success", error=None, data=reporting_orgs_list)
126+
return PaginatedResultsPage.create(reporting_orgs_list, paging.page, paging.page_size, total_records, request)
110127

111128

112129
@router.get("/{org_id}")

src/register_your_data_api/util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,9 @@ def __del__(self) -> None:
168168
pass
169169

170170
def _setup_client_application_details_provider(self) -> None:
171-
self._client_app_details = ClientApplicationDetailsProvider(self._env["CLIENT_APPLICATION_DETAILS_FILE"])
171+
self._client_app_details = ClientApplicationDetailsProvider(
172+
self._env["CLIENT_APPLICATION_DETAILS_FILE"], self._app_logger, self._audit_logger
173+
)
172174

173175
def _setup_prom_metrics(self) -> None:
174176
"""Add all the prometheus metrics"""

0 commit comments

Comments
 (0)