diff --git a/backend/config/settings.py b/backend/config/settings.py index a8b52f2af8..f00d9c9f5e 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -97,8 +97,10 @@ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", + "django.contrib.humanize", "django.contrib.sessions", "django.contrib.messages", + "django.contrib.postgres", "django.contrib.staticfiles", ] diff --git a/backend/dissemination/search.py b/backend/dissemination/search.py index 17c0881afc..a0a63a4bbe 100644 --- a/backend/dissemination/search.py +++ b/backend/dissemination/search.py @@ -17,7 +17,10 @@ def search_general( # TODO: use something like auditee_name__contains # SELECT * WHERE auditee_name LIKE '%SomeString%' if names: - names_match = Q(Q(auditee_name__in=names) | Q(auditor_firm_name__in=names)) + names_match = Q() + for name in names: + names_match.add(Q(auditee_name__search=name), Q.OR) + names_match.add(Q(auditor_firm_name__search=name), Q.OR) query.add(names_match, Q.AND) if uei_or_eins: diff --git a/backend/dissemination/templates/summary.html b/backend/dissemination/templates/summary.html index 29826ea9f2..ef003d7378 100644 --- a/backend/dissemination/templates/summary.html +++ b/backend/dissemination/templates/summary.html @@ -1,78 +1,424 @@ {% extends "base.html" %} -{% load static %} {% load field_name_to_label %} +{% load getkey %} +{% load humanize %} +{% load sprite_helper %} +{% load static %} {% block content %} -
+
-
- -
- {% comment %} General Information {% endcomment %} -
-

{{ auditee_name }} ({{ auditee_uei }})

-

- Report ID:{{ report_id }} + {% comment %} Title & Header {% endcomment %} +

+
+ Single audit summary + +

{{ auditee_name }}

+

+ UEI: {{ auditee_uei }} +

+
+
+

+ Report ID: {{ report_id }}

- Submission date:{{ general.submitted_date }} + Submission date: {{ general.submitted_date }}

- Fiscal Year:{{ general.fy_start_date }} to {{ general.fy_end_date }} + Fiscal Year: {{ general.fy_start_date }} to {{ general.fy_end_date }}

-
-

General Information

-
-
- {% for k, v in general.items %} -

- {{ k|field_name_to_label }}: {{ v }} -

- {% endfor %} -
-
+
+
+ - {% for section_key, section_list in data.items %} -
- -

{{ section_key }}

- | - {{ section_list|length }} -
-
- - - - {% for k in section_list.0 %}{% endfor %} - - - - {% for item in section_list %} - - {% for k, v in item.items %} - {% if v == '' %} - - {% else %} - - {% endif %} - {% endfor %} - - {% endfor %} - -
{{ k|field_name_to_label }}
-{{ v }}
-
-
- {% endfor %} -
+
+
+
+
+ + {% comment %} The grey box of doom {% endcomment %} +
+
+
+ {% comment %} Auditee {% endcomment %} +

Auditee

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ Contact Name: {{ general.auditee_contact_name }} +

+
+

+ Contact title: {{ general.auditee_contact_title }} +

+
+

+ Email: {{ general.auditee_email }} +

+
+

+ Phone: {{ general.auditee_phone }} +

+
+

+ Address: {{ general.auditee_address_line_1 }} +

+
+

+ City and state: {{ general.auditee_city }}, {{ general.auditee_state }} +

+
+

+ Zip code: {{ general.auditee_zip }} +

+
+

+ Additional UEIs?  + {% if data|getkey:"Additional UEIs" %} + Y + {% else %} + N + {% endif %} + + {% if not data|getkey:"Additional UEIs" %} + + + Download workbook 6 + + {% else %} + + + Download workbook 6 + + {% endif %} +

+
+

+ EIN: {{ general.auditee_ein }} +

+
+

+ + Additional EINs?  + {% if data|getkey:"Additional EINs" %} + Y + {% else %} + N + {% endif %} + + {% if not data|getkey:"Additional EINs" %} + + + Download workbook 8 + + {% else %} + + + Download workbook 8 + + {% endif %} +

+
+

+ Certifying name: {{ general.auditee_certify_name }} +

+
+

+ Certifying title: {{ general.auditee_certify_title }} +

+
+ + {% comment %} Auditor {% endcomment %} +

Auditor

+ + + + + + + + + + + + + + + + + + + +
+

+ Contact Name: {{ general.auditor_contact_name }} +

+
+

+ Contact title: {{ general.auditor_contact_title }} +

+
+

+ Email: {{ general.auditor_email }} +

+
+

+ Phone: {{ general.auditor_phone }} +

+
+

+ Address: {{ general.auditor_address_line_1 }} +

+
+

+ City and state: {{ general.auditor_city }}, {{ general.auditor_state }} +

+
+

+ Zip code: {{ general.auditor_zip }} +

+
+

+ Secondary Auditors?  + {% if data|getkey:"Secondary Auditors" %} + Y + {% else %} + N + {% endif %} + + {% if not data|getkey:"Secondary Auditors" %} + + + Download workbook 7 + + {% else %} + + + Download workbook 7 + + {% endif %} +

+
+ + {% comment %} Summary {% endcomment %} +

Summary

+ + + + + + + + + {% comment %} Findings text - If none, show the gray box with no link. {% endcomment %} + + + {% comment %} CAP - If none, show the gray box with no link. {% endcomment %} + + + + + +
+

+ Federal awards:  + {{ data|getkey:"Awards"|length }} + + + + Download workbook 1 + +

+
+

+ Notes to SEFA:  + {{ data|getkey:"Notes to SEFA"|length }} + + {% if not data|getkey:"Notes to SEFA" %} + + + Download workbook 2 + + {% else %} + + + Download workbook 2 + + {% endif%} +

+
+

+ Findings:  + {{ data|getkey:"Audit Findings"|length }} + + {% if not data|getkey:"Audit Findings" %} + + + Download workbook 3 + + {% else %} + + + Download workbook 3 + + {% endif %} +

+
+

+ Findings text:  + {{ data|getkey:"Audit Findings Text"|length }} + + {% if not data|getkey:"Audit Findings Text" %} + + + Download workbook 4 + + {% else %} + + + Download workbook 4 + + {% endif %} +

+
+

+ CAP:  + {{ data|getkey:"Corrective Action Plan"|length }} + + {% if not data|getkey:"Corrective Action Plan" %} + + + Download workbook 5 + + {% else %} + + + Download workbook 5 + + {% endif %} +

+
+

+ Total federal expenditure:  + + ${{ general.total_amount_expended|intcomma }} + +

+
- {% include "audit-metadata.html" %} {% endblock content %} diff --git a/backend/dissemination/templatetags/getkey.py b/backend/dissemination/templatetags/getkey.py new file mode 100644 index 0000000000..39075e0927 --- /dev/null +++ b/backend/dissemination/templatetags/getkey.py @@ -0,0 +1,14 @@ +""" +Custom tag to pull a key with a space out of a dictionary. +Example: +{{ data.'Notes to SEFA' }} does not work. +Instead, {{ data|getkey:"Notes to SEFA" }} +""" +from django import template + +register = template.Library() + + +@register.filter(name="getkey") +def getkey(value, arg): + return value.get(arg, []) diff --git a/backend/dissemination/test_search.py b/backend/dissemination/test_search.py index 57fc7ef725..e17e9cc46d 100644 --- a/backend/dissemination/test_search.py +++ b/backend/dissemination/test_search.py @@ -82,6 +82,32 @@ def test_name_multiple(self): assert_all_results_public(self, results) self.assertEqual(len(results), 2) + def test_name_matches_inexact(self): + """ + Given a partial name, search_general should return records whose name fields contain the term, even if not an exact match + """ + auditee_match = baker.make( + General, is_public=True, auditee_name="the university of somewhere" + ) + auditor_match = baker.make( + General, is_public=True, auditor_firm_name="auditors unite, LLC" + ) + baker.make(General, is_public=True, auditee_name="not looking for this auditee") + baker.make( + General, + is_public=True, + auditor_firm_name="not looking for this auditor firm", + ) + + results = search_general( + names=["UNIVERSITY", "unitE", "there is not match for this one"] + ) + + assert_all_results_public(self, results) + self.assertEqual(len(results), 2) + self.assertEqual(results[0], auditee_match) + self.assertEqual(results[1], auditor_match) + def test_uei_or_ein_matches_uei(self): """ Given a uei_or_ein, search_general should return records with a matching UEI @@ -194,13 +220,13 @@ def test_oversight_agency(self): baker.make(General, is_public=True, oversight_agency="02") results = search_general( - cog_or_oversight="cog", + cog_or_oversight="oversight", agency_name="01", ) assert_all_results_public(self, results) self.assertEqual(len(results), 1) - self.assertEqual(results[0].cognizant_agency, "01") + self.assertEqual(results[0].oversight_agency, "01") def test_audit_year(self): """ @@ -225,5 +251,3 @@ def test_audit_year(self): audit_years=[2020, 2021, 2022], ) self.assertEqual(len(results), 3) - - return diff --git a/backend/dissemination/test_views.py b/backend/dissemination/test_views.py new file mode 100644 index 0000000000..6c026bda07 --- /dev/null +++ b/backend/dissemination/test_views.py @@ -0,0 +1,146 @@ +from django.test import Client, TestCase +from django.urls import reverse + +from audit.models import ( + ExcelFile, + SingleAuditChecklist, + SingleAuditReportFile, + generate_sac_report_id, +) +from dissemination.models import General + +from model_bakery import baker +from unittest.mock import patch + + +class PdfDownloadViewTests(TestCase): + def setUp(self): + self.client = Client() + + def _make_sac_and_general(self): + sac = baker.make( + SingleAuditChecklist, + report_id=generate_sac_report_id(end_date="2023-12-31"), + ) + general = baker.make(General, is_public=True, report_id=sac.report_id) + return sac, general + + def test_bad_report_id_returns_404(self): + url = reverse("dissemination:PdfDownload", kwargs={"report_id": "not-real"}) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + def test_not_public_returns_403(self): + general = baker.make(General, is_public=False) + + url = reverse( + "dissemination:PdfDownload", kwargs={"report_id": general.report_id} + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 403) + + def test_no_file_returns_404(self): + sac, general = self._make_sac_and_general() + + url = reverse( + "dissemination:PdfDownload", kwargs={"report_id": general.report_id} + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + @patch("audit.file_downloads.file_exists") + def test_file_exists_returns_302(self, mock_file_exists): + mock_file_exists.return_value = True + + sac, general = self._make_sac_and_general() + + file = baker.make(SingleAuditReportFile, sac=sac) + + url = reverse( + "dissemination:PdfDownload", kwargs={"report_id": general.report_id} + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 302) + self.assertIn(file.filename, response.url) + + +class XlsxDownloadViewTests(TestCase): + def setUp(self): + self.client = Client() + + def _make_sac_and_general(self): + sac = baker.make( + SingleAuditChecklist, + report_id=generate_sac_report_id(end_date="2023-12-31"), + ) + general = baker.make(General, is_public=True, report_id=sac.report_id) + return sac, general + + def test_bad_report_id_returns_404(self): + url = reverse( + "dissemination:XlsxDownload", + kwargs={"report_id": "not-real", "file_type": "FederalAwardsExpended"}, + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + def test_not_public_returns_403(self): + general = baker.make(General, is_public=False) + + url = reverse( + "dissemination:XlsxDownload", + kwargs={ + "report_id": general.report_id, + "file_type": "FederalAwardsExpended", + }, + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 403) + + def test_no_file_returns_404(self): + sac, general = self._make_sac_and_general() + + url = reverse( + "dissemination:XlsxDownload", + kwargs={ + "report_id": general.report_id, + "file_type": "FederalAwardsExpended", + }, + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 404) + + @patch("audit.file_downloads.file_exists") + def test_file_exists_returns_302(self, mock_file_exists): + mock_file_exists.return_value = True + + sac, general = self._make_sac_and_general() + + file = baker.make(ExcelFile, sac=sac, form_section="FederalAwardsExpended") + + url = reverse( + "dissemination:XlsxDownload", + kwargs={ + "report_id": general.report_id, + "file_type": "FederalAwardsExpended", + }, + ) + + response = self.client.get(url) + + self.assertEqual(response.status_code, 302) + self.assertIn(file.filename, response.url) diff --git a/backend/dissemination/tests.py b/backend/dissemination/tests.py index 92b72c0807..86607de84e 100644 --- a/backend/dissemination/tests.py +++ b/backend/dissemination/tests.py @@ -13,7 +13,6 @@ from dissemination.models import ( General, FederalAward, - Passthrough, Finding, FindingText, CapText, @@ -124,7 +123,6 @@ def test_summary_context(self): """ baker.make(General, report_id="2022-12-GSAFAC-0000000001", is_public=True) award = baker.make(FederalAward, report_id="2022-12-GSAFAC-0000000001") - passthrough = baker.make(Passthrough, report_id="2022-12-GSAFAC-0000000001") finding = baker.make(Finding, report_id="2022-12-GSAFAC-0000000001") finding_text = baker.make(FindingText, report_id="2022-12-GSAFAC-0000000001") cap_text = baker.make(CapText, report_id="2022-12-GSAFAC-0000000001") @@ -139,10 +137,6 @@ def test_summary_context(self): response.context["data"]["Awards"][0]["additional_award_identification"], award.additional_award_identification, ) - self.assertEquals( - response.context["data"]["Passthrough Entities"][0]["award_reference"], - passthrough.award_reference, - ) self.assertEquals( response.context["data"]["Audit Findings"][0]["reference_number"], finding.reference_number, @@ -158,6 +152,6 @@ def test_summary_context(self): cap_text.contains_chart_or_table, ) self.assertEquals( - response.context["data"]["Notes"][0]["accounting_policies"], + response.context["data"]["Notes to SEFA"][0]["accounting_policies"], note.accounting_policies, ) diff --git a/backend/dissemination/urls.py b/backend/dissemination/urls.py index aeddd8c803..5613fde5e2 100644 --- a/backend/dissemination/urls.py +++ b/backend/dissemination/urls.py @@ -5,7 +5,16 @@ app_name = "dissemination" urlpatterns = [ - path("pdf/", views.PdfDownloadView.as_view(), name="PdfDownload"), + path( + "workbook/xlsx//", + views.XlsxDownloadView.as_view(), + name="XlsxDownload", + ), + path( + "report/pdf/", + views.PdfDownloadView.as_view(), + name="PdfDownload", + ), path("search/", views.Search.as_view(), name="Search"), path("summary/", views.AuditSummaryView.as_view(), name="Summary"), ] diff --git a/backend/dissemination/views.py b/backend/dissemination/views.py index 9b8744ff40..e3d55bc2d7 100644 --- a/backend/dissemination/views.py +++ b/backend/dissemination/views.py @@ -11,11 +11,13 @@ from dissemination.models import ( General, FederalAward, - Passthrough, Finding, FindingText, CapText, Note, + SecondaryAuditor, + AdditionalEin, + AdditionalUei, ) @@ -97,11 +99,13 @@ def get_audit_content(self, report_id): further. I.e. remove DB ids or something. """ awards = FederalAward.objects.filter(report_id=report_id) - passthrough_entities = Passthrough.objects.filter(report_id=report_id) audit_findings = Finding.objects.filter(report_id=report_id) audit_findings_text = FindingText.objects.filter(report_id=report_id) corrective_action_plan = CapText.objects.filter(report_id=report_id) notes_to_sefa = Note.objects.filter(report_id=report_id) + secondary_auditors = SecondaryAuditor.objects.filter(report_id=report_id) + additional_ueis = AdditionalUei.objects.filter(report_id=report_id) + additional_eins = AdditionalEin.objects.filter(report_id=report_id) data = {} @@ -109,8 +113,8 @@ def get_audit_content(self, report_id): x for x in awards.values() ] # Take QuerySet to a list of objects - if passthrough_entities.exists(): - data["Passthrough Entities"] = [x for x in passthrough_entities.values()] + if notes_to_sefa.exists(): + data["Notes to SEFA"] = [x for x in notes_to_sefa.values()] if audit_findings.exists(): data["Audit Findings"] = [x for x in audit_findings.values()] if audit_findings_text.exists(): @@ -119,13 +123,12 @@ def get_audit_content(self, report_id): data["Corrective Action Plan"] = [ x for x in corrective_action_plan.values() ] - if notes_to_sefa.exists(): - data["Notes"] = [x for x in notes_to_sefa.values()] - - for key in data: - for item in data[key]: - del item["id"] - del item["report_id"] + if secondary_auditors.exists(): + data["Secondary Auditors"] = [x for x in secondary_auditors.values()] + if additional_ueis.exists(): + data["Additional UEIs"] = [x for x in additional_ueis.values()] + if additional_eins.exists(): + data["Additional EINs"] = [x for x in additional_eins.values()] return data @@ -143,3 +146,18 @@ def get(self, request, report_id): filename = get_filename(sac, "report") return redirect(get_download_url(filename)) + + +class XlsxDownloadView(View): + def get(self, request, report_id, file_type): + # only allow xlsx downloads from disseminated submissions + disseminated = get_object_or_404(General, report_id=report_id) + + # only allow xlsx downloads for public submissions + if not disseminated.is_public: + raise PermissionDenied("You do not have access to this file.") + + sac = get_object_or_404(SingleAuditChecklist, report_id=report_id) + filename = get_filename(sac, file_type) + + return redirect(get_download_url(filename)) diff --git a/backend/static/scss/main.scss b/backend/static/scss/main.scss index 7bebb9a9bc..4e3d7c130b 100644 --- a/backend/static/scss/main.scss +++ b/backend/static/scss/main.scss @@ -3,9 +3,11 @@ $theme-font-path: '../fonts', $theme-image-path: '../img', $theme-font-type-sans: 'public-sans', + $theme-font-weight-semibold: 600, $theme-type-scale-md: 7, $theme-type-scale-2xl: 13, $theme-color-success-lighter: #eef8eb, + $theme-color-base-lighter: #efefef, ); @forward 'uswds'; @use '_home.scss'; diff --git a/backend/templates/audit-metadata.html b/backend/templates/audit-metadata.html index 203930b3a4..1797cfb12b 100644 --- a/backend/templates/audit-metadata.html +++ b/backend/templates/audit-metadata.html @@ -1,4 +1,4 @@ - +{% comment %} Audit metadata. Included at the bottom of several screens. {% endcomment %}