diff --git a/.cspell.json b/.cspell.json index 8b28b84137..398795018a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -203,6 +203,7 @@ "lokalize", "lookup", "lte", + "lxml", "MapItem", "mappable", "mapquest", @@ -235,6 +236,7 @@ "noqa", "npm", "nrows", + "nsmap", "num", "Octant", "officedocument", diff --git a/seed/audit_template/audit_template.py b/seed/audit_template/audit_template.py index 23f402344c..80a97cb96e 100644 --- a/seed/audit_template/audit_template.py +++ b/seed/audit_template/audit_template.py @@ -7,6 +7,7 @@ import json import logging from datetime import datetime +from typing import Any, Tuple import requests from celery import shared_task @@ -53,6 +54,41 @@ def get_building_xml(self, audit_template_building_id, token): return response, "" + def get_submission(self, audit_template_submission_id: int, report_format: str = 'pdf') -> Tuple[Any, str]: + """Download an Audit Template submission report. + + Args: + audit_template_submission_id (int): value of the "Submission ID" as seen on Audit Template + report_format (str, optional): Report format, either `xml` or `pdf`. Defaults to 'pdf'. + + Returns: + requests.response: Result from Audit Template website + """ + # supporting 'PDF' and 'XML' formats only for now + token, message = self.get_api_token() + if not token: + return None, message + + # validate format + if report_format.lower() not in ['xml', 'pdf']: + report_format = 'pdf' + + # set headers + headers = {'accept': 'application/pdf'} + if report_format.lower() == 'xml': + headers = {'accept': 'application/xml'} + + url = f'{self.API_URL}/rp/submissions/{audit_template_submission_id}.{report_format}?token={token}' + try: + response = requests.request("GET", url, headers=headers) + + if response.status_code != 200: + return None, f'Expected 200 response from Audit Template get_submission but got {response.status_code!r}: {response.content!r}' + except Exception as e: + return None, f'Unexpected error from Audit Template: {e}' + + return response, "" + def get_buildings(self, cycle_id): token, message = self.get_api_token() if not token: @@ -161,7 +197,7 @@ def build_xml(self, state, report_type, display_field): view = state.propertyview_set.first() gfa = state.gross_floor_area - if type(gfa) == int: + if isinstance(gfa, int): gross_floor_area = str(gfa) elif gfa.units != ureg.feet**2: gross_floor_area = str(gfa.to(ureg.feet ** 2).magnitude) diff --git a/seed/tests/test_audit_template.py b/seed/tests/test_audit_template.py index 4bce67b9ca..c7f1c1270c 100644 --- a/seed/tests/test_audit_template.py +++ b/seed/tests/test_audit_template.py @@ -44,6 +44,7 @@ def setUp(self): self.get_building_url = reverse('api:v3:audit_template-get-building-xml', args=['1']) self.get_buildings_url = reverse('api:v3:audit_template-get-buildings') + self.get_submission_url = reverse('api:v3:audit_template-get-submission', args=['1']) self.good_authenticate_response = mock.Mock() self.good_authenticate_response.status_code = 200 @@ -57,6 +58,11 @@ def setUp(self): self.good_get_building_response.status_code = 200 self.good_get_building_response.text = "building response" + self.good_get_submission_response = mock.Mock() + self.good_get_submission_response.status_code = 200 + self.good_get_submission_response.text = "submission response" + self.good_get_submission_response.content = "submission response" + self.bad_get_building_response = mock.Mock() self.bad_get_building_response.status_code = 400 self.bad_get_building_response.content = "bad building response" @@ -71,6 +77,16 @@ def test_get_building_xml_from_audit_template(self, mock_request): self.assertEqual(200, response.status_code, response.content) self.assertEqual(response.content, b"building response") + @mock.patch('requests.request') + def test_get_submission_from_audit_template(self, mock_request): + # -- Act + mock_request.side_effect = [self.good_authenticate_response, self.good_get_submission_response] + response = self.client.get(self.get_submission_url, data={"organization_id": self.org.id}) + + # -- Assert + self.assertEqual(200, response.status_code, response.content) + self.assertEqual(response.content, b"submission response") + @mock.patch('requests.request') def test_get_building_xml_from_audit_template_org_has_no_at_token(self, mock_request): # -- Setup diff --git a/seed/views/main.py b/seed/views/main.py index 93f30ab6c5..bcbadca90b 100644 --- a/seed/views/main.py +++ b/seed/views/main.py @@ -133,7 +133,7 @@ def health_check(request): celery_status = False try: - redis_status = not cache.has_key('redis-ping') + redis_status = 'redis-ping' not in cache except Exception: redis_status = False diff --git a/seed/views/v3/audit_template.py b/seed/views/v3/audit_template.py index 0cfecbf7d9..1dfb398d10 100644 --- a/seed/views/v3/audit_template.py +++ b/seed/views/v3/audit_template.py @@ -17,13 +17,57 @@ class AuditTemplateViewSet(viewsets.ViewSet, OrgMixin): + @swagger_auto_schema(manual_parameters=[ + AutoSchemaHelper.query_org_id_field(), + AutoSchemaHelper.base_field( + name='id', + location_attr='IN_PATH', + type='TYPE_INTEGER', + required=True, + description='Audit Template Submission ID.'), + AutoSchemaHelper.query_string_field('report_format', False, 'Report format Valid values are: xml, pdf. Defaults to pdf.') + ]) + @has_perm_class('can_view_data') + @action(detail=True, methods=['GET']) + def get_submission(self, request, pk): + """ + Fetches a Report Submission (XML or PDF) from Audit Template (only) + """ + # get report format or default to pdf + default_report_format = 'pdf' + report_format = request.query_params.get('report_format', default_report_format) + + valid_file_formats = ['xml', 'pdf'] + if report_format.lower() not in valid_file_formats: + message = f"The report_format specified is invalid. Must be one of: {valid_file_formats}." + return JsonResponse({ + 'success': False, + 'message': message + }, status=400) + + # retrieve report + at = AuditTemplate(self.get_organization(self.request)) + response, message = at.get_submission(pk, report_format) + + if response is None: + return JsonResponse({ + 'success': False, + 'message': message + }, status=400) + if report_format.lower() == 'xml': + return HttpResponse(response.text) + else: + response2 = HttpResponse(response.content) + response2.headers["Content-Type"] = 'application/pdf' + response2.headers["Content-Disposition"] = f'attachment; filename="at_submission_{pk}.pdf"' + return response2 @swagger_auto_schema(manual_parameters=[AutoSchemaHelper.query_org_id_field()]) @has_perm_class('can_view_data') @action(detail=True, methods=['GET']) def get_building_xml(self, request, pk): """ - Fetches a Building XML for an Audit Template property and updates the corresponding PropertyView + Fetches a Building XML for an Audit Template property (only) """ at = AuditTemplate(self.get_organization(self.request)) response, message = at.get_building(pk) @@ -145,7 +189,7 @@ def get_buildings(self, request): at = AuditTemplate(org) result = at.get_buildings(cycle_id) - if type(result) is tuple: + if isinstance(result, tuple): return JsonResponse({ 'success': False, 'message': result[1]