Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2023-10-26 | MAIN --> PROD | DEV (99bec76) --> STAGING #2623

Merged
merged 4 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 0 additions & 102 deletions backend/api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from rest_framework.test import APIClient

from api.test_uei import valid_uei_results
from api.views import SACViewSet
from audit.models import Access, SingleAuditChecklist

User = get_user_model()
Expand All @@ -18,7 +17,6 @@
ACCESS_AND_SUBMISSION_PATH = reverse("api-accessandsubmission")
SUBMISSIONS_PATH = reverse("submissions")
ACCESS_LIST_PATH = reverse("access-list")
SAC_LIST_PATH = reverse("sac-list")


VALID_AUDITEE_INFO_DATA = {
Expand Down Expand Up @@ -910,106 +908,6 @@ def test_deleted_access_not_returned(self):
self.assertEqual(data_2[0]["report_id"], access_1.sac.report_id)


class SACViewSetTests(TestCase):
def setUp(self):
self.client = APIClient()

def test_list_no_auth_required(self):
"""
The SACViewSet should not require authentication or permissions
"""
self.assertEqual(SACViewSet.authentication_classes, [])
self.assertEqual(SACViewSet.permission_classes, [])

def test_list_no_audits_returns_empty_list(self):
"""
If there are no SACs in the database, the list endpoint should return no results
"""
response = self.client.get(SAC_LIST_PATH)
data = response.json()

self.assertEqual(response.status_code, 200)
self.assertEqual(data, [])

def test_list_none_submitted_returns_empty_list(self):
"""
If there are SACs in the database, but none which have a status of submitted, the list endpoint shoul return no results
"""
for status in SingleAuditChecklist.STATUS_CHOICES:
if status[0] != SingleAuditChecklist.STATUS.SUBMITTED:
baker.make(
SingleAuditChecklist, _quantity=100, submission_status=status[0]
)

response = self.client.get(SAC_LIST_PATH)
data = response.json()

self.assertEqual(response.status_code, 200)
self.assertEqual(data, [])

def test_list_returns_only_submitted(self):
"""
If there are SACs in the database, only those with a submission_status of "submitted" should be returned
"""
for status in SingleAuditChecklist.STATUS_CHOICES:
baker.make(SingleAuditChecklist, _quantity=100, submission_status=status[0])

response = self.client.get(SAC_LIST_PATH)
data = response.json()

self.assertEqual(len(data), 100)
self.assertTrue(
all(audit["submission_status"] == "submitted" for audit in data)
)

def test_detail_no_match_returns_404(self):
"""
If there is no SAC matching the provided report_id, the detail endpoint should return 404
"""
url = reverse("sac-detail", kwargs={"report_id": "not-a-real-report-id"})

response = self.client.get(url)

self.assertEqual(response.status_code, 404)

def test_detail_match_submitted_returns_sac(self):
"""
If there is a SAC matching the provided report_id, and the SAC has a submission_status of submitted, the detail endpoint should return the SAC
"""
report_id = "test-report-id"
sac = baker.make(
SingleAuditChecklist, report_id=report_id, submission_status="submitted"
)

url = reverse("sac-detail", kwargs={"report_id": report_id})

response = self.client.get(url)
data = response.json()

self.assertEqual(response.status_code, 200)
self.assertTrue(data, sac)

def test_detail_match_unsubmitted_returns_404(self):
"""
If there is a SAC matching the provided report_id, and the SAC has a submission_status other than submitted, the detail endpoint should return a 404
"""
for status in SingleAuditChecklist.STATUS_CHOICES:
with self.subTest():
if status[0] != SingleAuditChecklist.STATUS.SUBMITTED:
report_id = f"id-{status[0]}"[:17]
baker.make(
SingleAuditChecklist,
report_id=report_id,
submission_status=status[0],
)

url = reverse("sac-detail", kwargs={"report_id": report_id})

response = self.client.get(url)

self.assertEqual(response.status_code, 404)


class SchemaViewTests(TestCase):
def setUp(self):
self.fiscal_years = [
Expand Down
30 changes: 1 addition & 29 deletions backend/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

from django.http import Http404, HttpResponse, JsonResponse
from django.urls import reverse
from django.views import View, generic
from django.views import generic
from django.contrib.auth import get_user_model
from rest_framework import viewsets
from rest_framework.authentication import BaseAuthentication
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -199,33 +198,6 @@ def get(self, _request):
)


class IndexView(View):
def get(self, request, *args, **kwargs):
fpath = BASE_DIR / "static" / "index.html"
return HttpResponse(content=fpath.read_text(encoding="utf-8"))


class SACViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows SACs to be viewed.
"""

# this is a public endpoint - no authentication or permission required
authentication_classes: List[BaseAuthentication] = []
permission_classes: List[BasePermission] = []

allowed_methods = ["GET"]

# lookup SACs with report_id rather than the default pk
lookup_field = "report_id"

queryset = SingleAuditChecklist.objects.filter(submission_status="submitted")
serializer_class = SingleAuditChecklistSerializer

def get_view_name(self):
return "SF-SAC"


class EligibilityFormView(APIView):
"""
Accepts information from Step 1 (Submission criteria check) of the "Create New Audit"
Expand Down
74 changes: 74 additions & 0 deletions backend/audit/file_downloads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import logging

from django.conf import settings
from django.http import Http404
from django.shortcuts import get_object_or_404

from boto3 import client as boto3_client
from botocore.client import ClientError, Config

from audit.models import ExcelFile, SingleAuditReportFile

logger = logging.getLogger(__name__)


def get_filename(sac, file_type):
if file_type == "report":
file_obj = get_object_or_404(SingleAuditReportFile, sac=sac)
return f"singleauditreport/{file_obj.filename}"
else:
file_obj = get_object_or_404(ExcelFile, sac=sac, form_section=file_type)
return f"excel/{file_obj.filename}"


def file_exists(filename):
# this client uses the internal endpoint url because we're making a request to S3 from within the app
s3_client = boto3_client(
service_name="s3",
region_name=settings.AWS_S3_PRIVATE_REGION_NAME,
aws_access_key_id=settings.AWS_PRIVATE_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_PRIVATE_SECRET_ACCESS_KEY,
endpoint_url=settings.AWS_S3_PRIVATE_INTERNAL_ENDPOINT,
config=Config(signature_version="s3v4"),
)

try:
s3_client.head_object(
Bucket=settings.AWS_PRIVATE_STORAGE_BUCKET_NAME,
Key=filename,
)

return True
except ClientError:
logger.warn(f"Unable to locate file {filename} in S3!")
return False


def get_download_url(filename):
try:
# this client uses the external endpoint url because we're generating a request URL that is eventually triggered from outside the app
s3_client = boto3_client(
service_name="s3",
region_name=settings.AWS_S3_PRIVATE_REGION_NAME,
aws_access_key_id=settings.AWS_PRIVATE_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_PRIVATE_SECRET_ACCESS_KEY,
endpoint_url=settings.AWS_S3_PRIVATE_EXTERNAL_ENDPOINT,
config=Config(signature_version="s3v4"),
)

if file_exists(filename):
response = s3_client.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": settings.AWS_PRIVATE_STORAGE_BUCKET_NAME,
"Key": filename,
"ResponseContentDisposition": f"attachment;filename={filename}",
},
ExpiresIn=30,
)

return response
else:
raise Http404("File not found")
except ClientError:
raise Http404("File not found")
59 changes: 59 additions & 0 deletions backend/audit/migrations/0005_alter_submissionevent_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 4.2.6 on 2023-10-24 18:58

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("audit", "0004_alter_singleauditchecklist_cognizant_agency_and_more"),
]

operations = [
migrations.AlterField(
model_name="submissionevent",
name="event",
field=models.CharField(
choices=[
("access-granted", "Access granted"),
("additional-eins-updated", "Additional EINs updated"),
("additional-ueis-updated", "Additional UEIs updated"),
("audit-information-updated", "Audit information updated"),
("audit-report-pdf-updated", "Audit report PDF updated"),
(
"auditee-certification-completed",
"Auditee certification completed",
),
(
"auditor-certification-completed",
"Auditor certification completed",
),
(
"corrective-action-plan-updated",
"Corrective action plan updated",
),
("created", "Created"),
("federal-awards-updated", "Federal awards updated"),
(
"federal-awards-audit-findings-updated",
"Federal awards audit findings updated",
),
(
"federal-awards-audit-findings-text-updated",
"Federal awards audit findings text updated",
),
(
"findings-uniform-guidance-updated",
"Findings uniform guidance updated",
),
("general-information-updated", "General information updated"),
("locked-for-certification", "Locked for certification"),
("unlocked-after-certification", "Unlocked after certification"),
("notes-to-sefa-updated", "Notes to SEFA updated"),
("secondary-auditors-updated", "Secondary auditors updated"),
("submitted", "Submitted to the FAC for processing"),
("disseminated", "Copied to dissemination tables"),
("tribal-consent-updated", "Tribal audit consent updated"),
]
),
),
]
8 changes: 8 additions & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@

AWS_S3_ENDPOINT_URL = AWS_S3_PRIVATE_ENDPOINT

# when running locally, the internal endpoint (docker network) is different from the external endpoint (host network)
AWS_S3_PRIVATE_INTERNAL_ENDPOINT = AWS_S3_ENDPOINT_URL
AWS_S3_PRIVATE_EXTERNAL_ENDPOINT = "http://localhost:9001"

DISABLE_AUTH = env.bool("DISABLE_AUTH", default=False)

# Used for backing up the database https://django-dbbackup.readthedocs.io/en/master/installation.html
Expand Down Expand Up @@ -309,6 +313,10 @@
AWS_S3_PRIVATE_ENDPOINT = s3_creds["endpoint"]
AWS_S3_ENDPOINT_URL = f"https://{AWS_S3_PRIVATE_ENDPOINT}"

# in deployed environments, the internal & external endpoint URLs are the same
AWS_S3_PRIVATE_INTERNAL_ENDPOINT = AWS_S3_ENDPOINT_URL
AWS_S3_PRIVATE_EXTERNAL_ENDPOINT = AWS_S3_ENDPOINT_URL

AWS_PRIVATE_LOCATION = "static"
AWS_PRIVATE_DEFAULT_ACL = "private"
# If wrong, https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl
Expand Down
8 changes: 1 addition & 7 deletions backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,7 @@
)

urlpatterns = [
# path("", IndexView.as_view(), name="index"),
path("api/schema.json", schema_view),
path("public/api/sac", views.SACViewSet.as_view({"get": "list"}), name="sac-list"),
path(
"public/api/sac/<str:report_id>",
views.SACViewSet.as_view({"get": "retrieve"}),
name="sac-detail",
),
path(
"api/sac/eligibility",
views.EligibilityFormView.as_view(),
Expand Down Expand Up @@ -79,6 +72,7 @@
name="sprite",
),
path("audit/", include("audit.urls")),
path("dissemination/", include("dissemination.urls")),
# Keep last so we can use short urls for content pages like home page etc.
path("", include("cms.urls")),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
Expand Down
16 changes: 16 additions & 0 deletions backend/dissemination/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django import forms


class SearchForm(forms.Form):
AY_choices = (
(x, str(x)) for x in range(2016, 2024)
) # ((2016, "2016"), (2017, "2017"), ..., (2023, "2023"))

entity_name = forms.CharField(required=False)
uei_or_ein = forms.CharField(required=False)
aln = forms.CharField(required=False)
start_date = forms.DateField(required=False)
end_date = forms.DateField(required=False)
cog_or_oversight = forms.CharField(required=False)
agency_name = forms.CharField(required=False)
audit_year = forms.MultipleChoiceField(choices=AY_choices, required=False)
Loading
Loading