From 6ede7f1a522a3141db03ace337c93448131c5f77 Mon Sep 17 00:00:00 2001 From: Carrie Arnold Date: Thu, 8 Jan 2026 14:25:02 -0600 Subject: [PATCH 1/3] testing the API connection --- apcd_cms/Dockerfile | 3 +- apcd_cms/src/apps/extension/views.py | 6 ++- .../src/apps/utils/testing_api_connection.py | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 apcd_cms/src/apps/utils/testing_api_connection.py diff --git a/apcd_cms/Dockerfile b/apcd_cms/Dockerfile index 18cd2d5..13de501 100644 --- a/apcd_cms/Dockerfile +++ b/apcd_cms/Dockerfile @@ -9,7 +9,8 @@ COPY /src/taccsite_cms /code/taccsite_cms # install node 20.x RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - -RUN apt-get install -y nodejs +RUN apt-get install -y nodejs python3 +RUN pip3 install httpx hvac RUN chmod u+x /code/client/build_client.sh && /code/client/build_client.sh RUN cp -R /code/client/dist/static/assets/. /code/taccsite_custom/apcd_cms/static/assets/ diff --git a/apcd_cms/src/apps/extension/views.py b/apcd_cms/src/apps/extension/views.py index 58c1992..cebbc07 100644 --- a/apcd_cms/src/apps/extension/views.py +++ b/apcd_cms/src/apps/extension/views.py @@ -10,6 +10,7 @@ from apps.utils.utils import table_filter from apps.utils.utils import title_case from apps.components.paginator.paginator import paginator +from apps.utils import testing_api_connection logger = logging.getLogger(__name__) @@ -24,9 +25,12 @@ class ExtensionFormApi(APCDGroupAccessAPIMixin, BaseAPIView): def get(self, request): if (request.user.is_authenticated and has_apcd_group(request.user)): + print("Testing API connection") + test_out = testing_api_connection.main() + print(test_out) formatted_extension_data = [] submitter_codes = [] - # submitter_id = request.GET.get('s_id', None) + #submitter_id = request.GET.get('s_id', None) submitters = apcd_database.get_submitter_info(request.user.username) submitter_codes = [] diff --git a/apcd_cms/src/apps/utils/testing_api_connection.py b/apcd_cms/src/apps/utils/testing_api_connection.py new file mode 100644 index 0000000..941804b --- /dev/null +++ b/apcd_cms/src/apps/utils/testing_api_connection.py @@ -0,0 +1,52 @@ +import os +import httpx + +from hvac import Client +from django.conf import settings + +print("testing api connection") +VAULT_ROLE = getattr(settings, 'VAULT_ROLE', '') +VAULT_SECRET = getattr(settings, 'VAULT_SECRET', '') + +client = Client(url='https://vault.txapcd.org') +client.auth.approle.login(role_id=VAULT_ROLE, secret_id=VAULT_SECRET, use_token=True) +vault_secret = client.secrets.kv.v2.read_secret(path='keys/api-key', mount_point='apcd') + +URL = 'https://apcd-etl-dev.txapcd.org' +USERNAME = 'test_submitter_user' +PASSWORD = vault_secret['data']['data']['api-key'] + + +def main(): + try: + with httpx.Client(timeout=5.0, base_url=URL) as client: + # Check connection without authentication + response = client.get('/') + print('GET request status:', response.status_code) + response.raise_for_status() + print('Response JSON:', response.json()) + + # Login and set authorization header + login_data = {'username': USERNAME, 'password': PASSWORD} + response = client.post('/auth/access-token', data=login_data) + print('\nPOST request status:', response.status_code) + response.raise_for_status() + token_response = response.json() + print('Created resource:', token_response) + auth_header = {'Authorization': f'Bearer {token_response["access_token"]}'} + client.headers.update(auth_header) + + # Example GET request with authentication + response = client.get('/users?limit=2') + print('\nGET request status with auth:', response.status_code) + response.raise_for_status() + print('Users:', response.json()) + + except httpx.RequestError as exc: + print(f'An error occurred while requesting {exc.request.url!r}: {exc}') + + except httpx.HTTPStatusError as exc: + print(f'Error response {exc.response.status_code} while requesting {exc.request.url!r}') + +if __name__ == '__main__': + main() From e426c9ccca70aa4a746fab3aaf52bb655e7e0da5 Mon Sep 17 00:00:00 2001 From: Garrett Edmonds Date: Thu, 5 Mar 2026 14:16:28 -0600 Subject: [PATCH 2/3] APCD API functions --- apcd_cms/src/apps/utils/apcd_api.py | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 apcd_cms/src/apps/utils/apcd_api.py diff --git a/apcd_cms/src/apps/utils/apcd_api.py b/apcd_cms/src/apps/utils/apcd_api.py new file mode 100644 index 0000000..e2b6010 --- /dev/null +++ b/apcd_cms/src/apps/utils/apcd_api.py @@ -0,0 +1,88 @@ +import os + +import httpx +import hvac +from django.conf import settings + + +VAULT_URL = 'https://vault.txapcd.org' +VAULT_ROLE = getattr(settings, 'VAULT_ROLE', '') +VAULT_SECRET = getattr(settings, 'VAULT_SECRET', '') +API_URL = 'https://apcd-etl-dev.txapcd.org' +API_USER = 'gedmonds' + + +def get_api_key_from_vault() -> str: + """Get the API key from HashiCorp Vault.""" + vault = hvac.Client(url=VAULT_URL) + vault.auth.approle.login(role_id=VAULT_ROLE, secret_id=VAULT_SECRET, use_token=True) + vault_secret = vault.secrets.kv.v2.read_secret(path='keys/api-key', mount_point='apcd') + print('api key:',vault_secret['data']['data']['api-key']) + return vault_secret['data']['data']['api-key'] + + +def login(username: str) -> None: + API_KEY = get_api_key_from_vault() + """Login and return api access token.""" + login_data = {'username': username, 'password': API_KEY} + response = api_client(url='/auth/access-token', method='post', data=login_data) + print('\nPOST request status:', response.status_code) + response.raise_for_status() + token_response = response.json() + print('Created resource:', token_response) + return token_response['access_token'] + + +def ping(client: httpx.Client) -> None: + """Ping the API to check connectivity.""" + response = client.get('') + print('\nGET /root request status:', response.status_code) + response.raise_for_status() + print('Ping Response:', response.json()) + + +def status(client: httpx.Client) -> None: + """Check API status.""" + response = client.get('/status') + print('\nGET /status request status:', response.status_code) + response.raise_for_status() + print('API Status:', response.json()) + +def me(client: httpx.Client) -> None: + """Get current user information.""" + response = client.get('/users/me') + print('\nGET /users/me request status:', response.status_code) + response.raise_for_status() + print('Current User Info:', response.json()) + + +def sub_exc(token: str) -> None: + """Get submitter exceptions information.""" + response = api_client(url='/submitter-exceptions/paged_view?page=1&per_page=50', method='get', token=token) + print('\nGET /submitter-exceptions request status:', response.status_code) + response.raise_for_status() + print('Submitter Exceptions Data:', response.json()) + return response.json() + + +def api_client(url: str, method: str, data: object = None, token: str = None) -> None: + response = {} + try: + with httpx.Client(timeout=5.0, base_url=API_URL) as client: + if not 'auth' in url: + client.headers.update({'Authorization': f'Bearer {token}'}) + if method == 'get': + response = client.get(url) + if method == 'post': + response = client.post(url, data=data) + except httpx.RequestError as exc: + print(f'An error occurred while requesting {exc.request.url!r}: {exc}') + response = exc.response + except httpx.HTTPStatusError as exc: + print(f'Error response {exc.response.status_code} while requesting {exc.request.url!r}') + response = exc.response + return response + + +if __name__ == '__main__': + main() \ No newline at end of file From 1a8bcb34e4bef7680842d10a4c1c855ad9c77178 Mon Sep 17 00:00:00 2001 From: Garrett Edmonds Date: Thu, 5 Mar 2026 16:59:50 -0600 Subject: [PATCH 3/3] Use api response in exception and admin exception listing views --- apcd_cms/src/apps/admin_exception/views.py | 79 +++++--------------- apcd_cms/src/apps/exception/views.py | 83 +++++----------------- 2 files changed, 33 insertions(+), 129 deletions(-) diff --git a/apcd_cms/src/apps/admin_exception/views.py b/apcd_cms/src/apps/admin_exception/views.py index 18ee593..91dd7ea 100644 --- a/apcd_cms/src/apps/admin_exception/views.py +++ b/apcd_cms/src/apps/admin_exception/views.py @@ -4,6 +4,7 @@ from apps.utils.utils import title_case, table_filter from apps.components.paginator.paginator import paginator from dateutil import parser +from apps.utils import apcd_api from apps.base.base import BaseAPIView, APCDAdminAccessAPIMixin, APCDAdminAccessTemplateMixin import logging import json @@ -18,9 +19,11 @@ class AdminExceptionsTable(APCDAdminAccessTemplateMixin, TemplateView): class AdminExceptionsApi(APCDAdminAccessAPIMixin, BaseAPIView): def get(self, *args, **kwargs): - exception_content = get_all_exceptions() + key = apcd_api.login('test_apcd_admin') + exceptions = apcd_api.sub_exc(key) # will be (request.user.apcd_api_key) or something like that - context = self.get_exception_list_json(exception_content, *args, **kwargs) + + context = self.get_exception_list_json(exceptions) return JsonResponse({'response': context}) def get_exception_list_json(self, exception_content, *args, **kwargs): @@ -44,71 +47,28 @@ def get_exception_list_json(self, exception_content, *args, **kwargs): org_filter = self.request.GET.get('org') def _set_exception(exception): return { - 'exception_id': exception[0], - 'submitter_id': exception[1], - 'requestor_name': exception[2], - 'request_type': title_case(exception[3]) if exception[3] else None, # to make sure if val doesn't exist, utils don't break page - 'explanation_justification': exception[4], - 'outcome': title_case(exception[5]) if exception[5] else 'None', - 'outcome': title_case(exception[5]) if exception[5] else 'None', - 'created_at': exception[6], - 'updated_at': exception[7], - 'submitter_code': exception[8], - 'payor_code': exception[9], - 'user_id': exception[10], - 'requestor_email': exception[11], - 'data_file': exception[12], - 'field_number': exception[13], - 'required_threshold': exception[14], - 'requested_threshold': exception[15], - 'requested_expiration_date': exception[16], - 'approved_threshold': exception[17], - 'approved_expiration_date': exception[18], - 'status': title_case(exception[19]) if exception[19] else 'None', - 'notes': exception[20], - 'entity_name': exception[21], - 'data_file_name': exception[22], - 'view_modal_content': { - 'exception_id': exception[0], - 'created_at': exception[6], - 'requestor_name': exception[2], - 'requestor_email': exception[11], - 'request_type': title_case(exception[3]) if exception[3] else None, - 'status': title_case(exception[19]) if exception[3] else None, - 'outcome': title_case(exception[5]) if exception[3] else None, - 'data_file_name': exception[22], - 'field_number': exception[13], - 'required_threshold': exception[14], - 'requested_threshold': exception[15], - 'approved_threshold': exception[17], - 'requested_expiration_date': exception[16], - 'approved_expiration_date': exception[18], - 'explanation_justification': exception[4], - 'notes': exception[20], - 'entity_name': exception[21], - 'payor_code': exception[9], - 'updated_at': exception[7], - } + **exception, + 'view_modal_content': exception } def getDate(row): date = row[6] return date if date is not None else parser.parse('1-1-0001') # sort exceptions by newest to oldest - exception_content = sorted(exception_content, key=lambda row:getDate(row), reverse=True) + #exception_content = sorted(exception_content, key=lambda row:getDate(row), reverse=True) limit = 50 offset = limit * (page_num - 1) exception_table_entries = [] - for exception in exception_content: + for exception in exception_content['records']: # to be used by paginator exception_table_entries.append(_set_exception(exception)) # to be able to access any exception in a template using exceptions var in the future context['exceptions'].append(_set_exception(exception)) - entity_name = title_case(exception[21]) - status = title_case(exception[19]) if exception[19] else 'None' - outcome = title_case(exception[5]) if exception[5] else 'None' + entity_name = title_case(exception["entity_name"]) + status = title_case(exception["status"]) if exception["status"] else 'None' + outcome = title_case(exception["outcome"]) if exception["outcome"] else 'None' if entity_name not in context['org_options']: context['org_options'].append(entity_name) # to make sure All is first in the dropdown filter options after sorting alphabetically @@ -134,16 +94,11 @@ def getDate(row): exception_table_entries = table_filter(org_filter.replace("(", "").replace(")",""), exception_table_entries, 'entity_name') context['query_str'] = queryStr - page_info = paginator(page_num, exception_table_entries, limit) - context['page'] = [{'entity_name': obj['entity_name'], 'payor_code': obj['payor_code'], 'created_at': obj['created_at'], 'request_type': obj['request_type'], - 'requestor_name': obj['requestor_name'], 'outcome': obj['outcome'], 'status': obj['status'], - 'approved_threshold': obj['approved_threshold'],'approved_expiration_date': obj['approved_expiration_date'], - 'notes': obj['notes'], 'exception_id': obj['exception_id'], 'view_modal_content': obj['view_modal_content'], - 'requested_threshold': obj['requested_threshold'],} - for obj in page_info['page']] - - context['page_num'] = page_num - context['total_pages'] = page_info['page'].paginator.num_pages + #page_info = paginator(page_num, exception_table_entries, limit) + context['page'] = exception_table_entries + + context['page_num'] = exception_content['current_page'] + context['total_pages'] = exception_content['total_pages'] context['pagination_url_namespaces'] = 'admin_exception:list-exceptions' diff --git a/apcd_cms/src/apps/exception/views.py b/apcd_cms/src/apps/exception/views.py index 5d17767..b420f9c 100644 --- a/apcd_cms/src/apps/exception/views.py +++ b/apcd_cms/src/apps/exception/views.py @@ -8,6 +8,7 @@ from apps.utils.utils import title_case, table_filter from apps.components.paginator.paginator import paginator from dateutil import parser +from apps.utils import apcd_api logger = logging.getLogger(__name__) @@ -22,16 +23,10 @@ class ExceptionFormApi(APCDGroupAccessAPIMixin, BaseAPIView): def get(self, request): if (request.user.is_authenticated and has_apcd_group(request.user)): - formatted_exception_data = [] - submitter_codes = [] - submitters = apcd_database.get_submitter_info(request.user.username) - submitter_codes = [] - - for submitter in submitters: - submitter_codes.append(submitter[1]) + key = apcd_api.login(request.user.username) + exceptions = apcd_api.sub_exc(key) # will be (request.user.apcd_api_key) or something like that - formatted_exception_data = apcd_database.get_all_exceptions(submitter_codes=submitter_codes) - context = self.get_exception_list_json(formatted_exception_data) + context = self.get_exception_list_json(exceptions) return JsonResponse({'response': context}) else: return JsonResponse({'error': 'Unauthorized'}, status=403) @@ -57,69 +52,28 @@ def get_exception_list_json(self, exception_content, *args, **kwargs): org_filter = self.request.GET.get('org') def _set_exception(exception): return { - 'exception_id': exception[0], - 'submitter_id': exception[1], - 'requestor_name': exception[2], - 'request_type': title_case(exception[3]) if exception[3] else None, # to make sure if val doesn't exist, utils don't break page - 'explanation_justification': exception[4], - 'outcome': title_case(exception[5]) if exception[5] else 'None', - 'outcome': title_case(exception[5]) if exception[5] else 'None', - 'created_at': exception[6], - 'updated_at': exception[7], - 'submitter_code': exception[8], - 'payor_code': exception[9], - 'user_id': exception[10], - 'requestor_email': exception[11], - 'data_file': exception[12], - 'field_number': exception[13], - 'required_threshold': exception[14], - 'requested_threshold': exception[15], - 'requested_expiration_date': exception[16], - 'approved_threshold': exception[17], - 'approved_expiration_date': exception[18], - 'status': title_case(exception[19]) if exception[19] else 'None', - 'entity_name': exception[21], - 'data_file_name': exception[22], - 'view_modal_content': { - 'exception_id': exception[0], - 'created_at': exception[6], - 'requestor_name': exception[2], - 'requestor_email': exception[11], - 'request_type': title_case(exception[3]) if exception[3] else None, - 'status': title_case(exception[19]) if exception[3] else None, - 'outcome': title_case(exception[5]) if exception[3] else None, - 'data_file_name': exception[22], - 'field_number': exception[13], - 'required_threshold': exception[14], - 'requested_threshold': exception[15], - 'approved_threshold': exception[17], - 'requested_expiration_date': exception[16], - 'approved_expiration_date': exception[18], - 'explanation_justification': exception[4], - 'entity_name': exception[21], - 'payor_code': exception[9], - 'updated_at': exception[7], - } + **exception, + 'view_modal_content': exception } def getDate(row): date = row[6] return date if date is not None else parser.parse('1-1-0001') # sort exceptions by newest to oldest - exception_content = sorted(exception_content, key=lambda row:getDate(row), reverse=True) + #exception_content = sorted(exception_content, key=lambda row:getDate(row), reverse=True) limit = 50 offset = limit * (page_num - 1) exception_table_entries = [] - for exception in exception_content: + for exception in exception_content['records']: # to be used by paginator exception_table_entries.append(_set_exception(exception)) # to be able to access any exception in a template using exceptions var in the future context['exceptions'].append(_set_exception(exception)) - entity_name = title_case(exception[21]) - status = title_case(exception[19]) if exception[19] else 'None' - outcome = title_case(exception[5]) if exception[5] else 'None' + entity_name = title_case(exception["entity_name"]) + status = title_case(exception["status"]) if exception["status"] else 'None' + outcome = title_case(exception["outcome"]) if exception["outcome"] else 'None' if entity_name not in context['org_options']: context['org_options'].append(entity_name) # to make sure All is first in the dropdown filter options after sorting alphabetically @@ -145,16 +99,11 @@ def getDate(row): exception_table_entries = table_filter(org_filter.replace("(", "").replace(")",""), exception_table_entries, 'entity_name') context['query_str'] = queryStr - page_info = paginator(page_num, exception_table_entries, limit) - context['page'] = [{'entity_name': obj['entity_name'], 'payor_code': obj['payor_code'], 'created_at': obj['created_at'], 'request_type': obj['request_type'], - 'requestor_name': obj['requestor_name'], 'outcome': obj['outcome'], 'status': obj['status'], - 'approved_threshold': obj['approved_threshold'],'approved_expiration_date': obj['approved_expiration_date'], - 'exception_id': obj['exception_id'], 'view_modal_content': obj['view_modal_content'], - 'requested_threshold': obj['requested_threshold'],} - for obj in page_info['page']] - - context['page_num'] = page_num - context['total_pages'] = page_info['page'].paginator.num_pages + #page_info = paginator(page_num, exception_table_entries, limit) + context['page'] = exception_table_entries + + context['page_num'] = exception_content['current_page'] + context['total_pages'] = exception_content['total_pages'] context['pagination_url_namespaces'] = 'exception:exception-list'