diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 04f5417b0..93b8359bf 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -513,9 +513,18 @@ function initializeWidgetOnList(list, parentId) { var readonlyView = document.querySelector("#action-needed-reason-email-readonly"); let emailWasSent = document.getElementById("action-needed-email-sent"); - let actionNeededEmailData = document.getElementById('action-needed-emails-data').textContent; - let actionNeededEmailsJson = JSON.parse(actionNeededEmailData); + let emailData = document.getElementById('action-needed-emails-data'); + if (!emailData) { + return; + } + + let actionNeededEmailData = emailData.textContent; + if(!actionNeededEmailData) { + return; + } + + let actionNeededEmailsJson = JSON.parse(actionNeededEmailData); const domainRequestId = actionNeededReasonDropdown ? document.querySelector("#domain_request_id").value : null const emailSentSessionVariableName = `actionNeededEmailSent-${domainRequestId}`; const oldDropdownValue = actionNeededReasonDropdown ? actionNeededReasonDropdown.value : null; @@ -750,3 +759,118 @@ function initializeWidgetOnList(list, parentId) { }); } })(); + + +/** An IIFE for dynamically changing some fields on the portfolio admin model +*/ +(function dynamicPortfolioFields(){ + + document.addEventListener('DOMContentLoaded', function() { + + let isPortfolioPage = document.getElementById("portfolio_form"); + if (!isPortfolioPage) { + return; + } + + // $ symbolically denotes that this is using jQuery + let $federalAgency = django.jQuery("#id_federal_agency"); + let organizationType = document.getElementById("id_organization_type"); + if ($federalAgency && organizationType) { + // Execute this function once on load + handleFederalAgencyChange($federalAgency, organizationType); + + // Attach the change event listener + $federalAgency.on("change", function() { + handleFederalAgencyChange($federalAgency, organizationType); + }); + } + + // Handle dynamically hiding the urbanization field + let urbanizationField = document.querySelector(".field-urbanization"); + let stateTerritory = document.getElementById("id_state_territory"); + if (urbanizationField && stateTerritory) { + // Execute this function once on load + handleStateTerritoryChange(stateTerritory, urbanizationField); + + // Attach the change event listener for state/territory + stateTerritory.addEventListener("change", function() { + handleStateTerritoryChange(stateTerritory, urbanizationField); + }); + } + }); + + function handleFederalAgencyChange(federalAgency, organizationType) { + // Set the org type to federal if an agency is selected + let selectedText = federalAgency.find("option:selected").text(); + + // There isn't a federal senior official associated with null records + if (!selectedText) { + return; + } + + if (selectedText !== "Non-Federal Agency") { + if (organizationType.value !== "federal") { + organizationType.value = "federal"; + } + }else { + if (organizationType.value === "federal") { + organizationType.value = ""; + } + } + + // Get the associated senior official with this federal agency + let $seniorOfficial = django.jQuery("#id_senior_official"); + if (!$seniorOfficial) { + console.log("Could not find the senior official field"); + return; + } + + let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value; + fetch(`${seniorOfficialApi}?agency_name=${selectedText}`) + .then(response => { + const statusCode = response.status; + return response.json().then(data => ({ statusCode, data })); + }) + .then(({ statusCode, data }) => { + if (data.error) { + // Clear the field if the SO doesn't exist. + if (statusCode === 404) { + $seniorOfficial.val("").trigger("change"); + console.warn("Record not found: " + data.error); + }else { + console.error("Error in AJAX call: " + data.error); + } + return; + } + + let seniorOfficialId = data.id; + let seniorOfficialName = [data.first_name, data.last_name].join(" "); + if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){ + // Clear the field if the SO doesn't exist + $seniorOfficial.val("").trigger("change"); + return; + } + + // Add the senior official to the dropdown. + // This format supports select2 - if we decide to convert this field in the future. + if ($seniorOfficial.find(`option[value='${seniorOfficialId}']`).length) { + // Select the value that is associated with the current Senior Official. + $seniorOfficial.val(seniorOfficialId).trigger("change"); + } else { + // Create a DOM Option that matches the desired Senior Official. Then append it and select it. + let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true); + $seniorOfficial.append(userOption).trigger("change"); + } + }) + .catch(error => console.error("Error fetching senior official: ", error)); + } + + function handleStateTerritoryChange(stateTerritory, urbanizationField) { + let selectedValue = stateTerritory.value; + if (selectedValue === "PR") { + showElement(urbanizationField) + } else { + hideElement(urbanizationField) + } + } +})(); diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index d238823b3..f3b41eb51 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1183,8 +1183,19 @@ document.addEventListener('DOMContentLoaded', function() { * @param {*} portfolio - the portfolio id */ function loadDomains(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, status = currentStatus, searchTerm = currentSearchTerm, portfolio = portfolioValue) { + // fetch json of page of domais, given params + let baseUrl = document.getElementById("get_domains_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + // fetch json of page of domains, given params - let url = `/get-domains-json/?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` + let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&status=${status}&search_term=${searchTerm}` if (portfolio) url += `&portfolio=${portfolio}` @@ -1524,7 +1535,17 @@ document.addEventListener('DOMContentLoaded', function() { */ function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) { // fetch json of page of domain requests, given params - fetch(`/get-domain-requests-json/?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`) + let baseUrl = document.getElementById("get_domain_requests_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + fetch(`${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`) .then(response => response.json()) .then(data => { if (data.error) { diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 3d13cf729..59f52cd95 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -24,6 +24,7 @@ from registrar.views.report_views import ( from registrar.views.domain_request import Step from registrar.views.domain_requests_json import get_domain_requests_json +from registrar.views.utility.api_views import get_senior_official_from_federal_agency_json from registrar.views.domains_json import get_domains_json from registrar.views.utility import always_404 from api.views import available, get_current_federal, get_current_full @@ -128,6 +129,11 @@ urlpatterns = [ AnalyticsView.as_view(), name="analytics", ), + path( + "admin/api/get-senior-official-from-federal-agency-json/", + get_senior_official_from_federal_agency_json, + name="get-senior-official-from-federal-agency-json", + ), path("admin/", admin.site.urls), path( "reports/export_data_type_user/", diff --git a/src/registrar/models/portfolio.py b/src/registrar/models/portfolio.py index 25429c0fe..fd5c3870d 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -148,3 +148,12 @@ class Portfolio(TimeStampedModel): def get_suborganizations(self): """Returns all suborganizations associated with this portfolio""" return self.portfolio_suborganizations.all() + + def save(self, *args, **kwargs): + """Save override for custom properties""" + + # The urbanization field is only intended for the state_territory puerto rico + if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization: + self.urbanization = None + + super().save(*args, **kwargs) diff --git a/src/registrar/templates/django/admin/portfolio_change_form.html b/src/registrar/templates/django/admin/portfolio_change_form.html index d9bf5cf01..9d59aae42 100644 --- a/src/registrar/templates/django/admin/portfolio_change_form.html +++ b/src/registrar/templates/django/admin/portfolio_change_form.html @@ -1,6 +1,13 @@ {% extends 'django/admin/email_clipboard_change_form.html' %} {% load i18n static %} +{% block content %} + {% comment %} Stores the json endpoint in a url for easier access {% endcomment %} + {% url 'get-senior-official-from-federal-agency-json' as url %} + + {{ block.super }} +{% endblock content %} + {% block field_sets %} {% for fieldset in adminform %} {% comment %} diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html index 30c206741..487c1cee5 100644 --- a/src/registrar/templates/includes/domain_requests_table.html +++ b/src/registrar/templates/includes/domain_requests_table.html @@ -1,5 +1,8 @@ {% load static %} +{% comment %} Stores the json endpoint in a url for easier access {% endcomment %} +{% url 'get_domain_requests_json' as url %} +
{% if not has_domain_requests_portfolio_permission %} diff --git a/src/registrar/templates/includes/domains_table.html b/src/registrar/templates/includes/domains_table.html index 4f0e20bde..73331c3f0 100644 --- a/src/registrar/templates/includes/domains_table.html +++ b/src/registrar/templates/includes/domains_table.html @@ -1,5 +1,10 @@ {% load static %} + + +{% comment %} Stores the json endpoint in a url for easier access {% endcomment %} +{% url 'get_domains_json' as url %} +
{% if not has_domains_portfolio_permission %} diff --git a/src/registrar/tests/test_api.py b/src/registrar/tests/test_api.py new file mode 100644 index 000000000..0025bc902 --- /dev/null +++ b/src/registrar/tests/test_api.py @@ -0,0 +1,67 @@ +from django.urls import reverse +from django.test import TestCase, Client +from registrar.models import FederalAgency, SeniorOfficial, User +from django.contrib.auth import get_user_model +from registrar.tests.common import create_superuser, create_user + + +class GetSeniorOfficialJsonTest(TestCase): + def setUp(self): + self.client = Client() + p = "password" + self.user = get_user_model().objects.create_user(username="testuser", password=p) + + self.superuser = create_superuser() + self.analyst_user = create_user() + + self.agency = FederalAgency.objects.create(agency="Test Agency") + self.senior_official = SeniorOfficial.objects.create( + first_name="John", last_name="Doe", title="Director", federal_agency=self.agency + ) + + self.api_url = reverse("get-senior-official-from-federal-agency-json") + + def tearDown(self): + User.objects.all().delete() + SeniorOfficial.objects.all().delete() + FederalAgency.objects.all().delete() + + def test_get_senior_official_json_authenticated_superuser(self): + """Test that a superuser can fetch the senior official information.""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get(self.api_url, {"agency_name": "Test Agency"}) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["id"], self.senior_official.id) + self.assertEqual(data["first_name"], "John") + self.assertEqual(data["last_name"], "Doe") + self.assertEqual(data["title"], "Director") + + def test_get_senior_official_json_authenticated_analyst(self): + """Test that an analyst user can fetch the senior official's information.""" + p = "userpass" + self.client.login(username="staffuser", password=p) + response = self.client.get(self.api_url, {"agency_name": "Test Agency"}) + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["id"], self.senior_official.id) + self.assertEqual(data["first_name"], "John") + self.assertEqual(data["last_name"], "Doe") + self.assertEqual(data["title"], "Director") + + def test_get_senior_official_json_unauthenticated(self): + """Test that an unauthenticated user receives a 403 with an error message.""" + p = "password" + self.client.login(username="testuser", password=p) + response = self.client.get(self.api_url, {"agency_name": "Test Agency"}) + self.assertEqual(response.status_code, 302) + + def test_get_senior_official_json_not_found(self): + """Test that a request for a non-existent agency returns a 404 with an error message.""" + p = "adminpass" + self.client.login(username="superuser", password=p) + response = self.client.get(self.api_url, {"agency_name": "Non-Federal Agency"}) + self.assertEqual(response.status_code, 404) + data = response.json() + self.assertEqual(data["error"], "Senior Official not found") diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 5167aac99..f4e998fff 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -2266,3 +2266,57 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.generic_org_type = None self.domain_request.save() self.assertFalse(self.domain_request._form_complete(request)) + + +class TestPortfolio(TestCase): + def setUp(self): + self.user, _ = User.objects.get_or_create( + username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World" + ) + super().setUp() + + def tearDown(self): + super().tearDown() + Portfolio.objects.all().delete() + User.objects.all().delete() + + def test_urbanization_field_resets_when_not_puetro_rico(self): + """The urbanization field should only be populated when the state is puetro rico. + Otherwise, this field should be empty.""" + # Start out as PR, then change the field + portfolio = Portfolio.objects.create( + creator=self.user, + organization_name="Test Portfolio", + state_territory=DomainRequest.StateTerritoryChoices.PUERTO_RICO, + urbanization="test", + ) + + self.assertEqual(portfolio.urbanization, "test") + self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO) + + portfolio.state_territory = DomainRequest.StateTerritoryChoices.ALABAMA + portfolio.save() + + self.assertEqual(portfolio.urbanization, None) + self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA) + + def test_can_add_urbanization_field(self): + """Ensures that you can populate the urbanization field when conditions are right""" + # Create a portfolio that cannot have this field + portfolio = Portfolio.objects.create( + creator=self.user, + organization_name="Test Portfolio", + state_territory=DomainRequest.StateTerritoryChoices.ALABAMA, + urbanization="test", + ) + + # Implicitly check if this gets cleared on create. It should. + self.assertEqual(portfolio.urbanization, None) + self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA) + + portfolio.state_territory = DomainRequest.StateTerritoryChoices.PUERTO_RICO + portfolio.urbanization = "test123" + portfolio.save() + + self.assertEqual(portfolio.urbanization, "test123") + self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO) diff --git a/src/registrar/views/utility/__init__.py b/src/registrar/views/utility/__init__.py index 7299c0368..7219f4358 100644 --- a/src/registrar/views/utility/__init__.py +++ b/src/registrar/views/utility/__init__.py @@ -8,3 +8,4 @@ from .permission_views import ( DomainInvitationPermissionDeleteView, DomainRequestWizardPermissionView, ) +from .api_views import get_senior_official_from_federal_agency_json diff --git a/src/registrar/views/utility/api_views.py b/src/registrar/views/utility/api_views.py new file mode 100644 index 000000000..2c9414d1d --- /dev/null +++ b/src/registrar/views/utility/api_views.py @@ -0,0 +1,36 @@ +import logging +from django.http import JsonResponse +from django.forms.models import model_to_dict +from registrar.models import FederalAgency, SeniorOfficial +from django.contrib.admin.views.decorators import staff_member_required +from django.contrib.auth.decorators import login_required + +logger = logging.getLogger(__name__) + + +@login_required +@staff_member_required +def get_senior_official_from_federal_agency_json(request): + """Returns federal_agency information as a JSON""" + + # This API is only accessible to admins and analysts + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if not request.user.is_authenticated or not any([analyst_perm, superuser_perm]): + return JsonResponse({"error": "You do not have access to this resource"}, status=403) + + agency_name = request.GET.get("agency_name") + agency = FederalAgency.objects.filter(agency=agency_name).first() + senior_official = SeniorOfficial.objects.filter(federal_agency=agency).first() + if agency and senior_official: + # Convert the agency object to a dictionary + so_dict = model_to_dict(senior_official) + + # The phone number field isn't json serializable, so we + # convert this to a string first if it exists. + if "phone" in so_dict and so_dict.get("phone"): + so_dict["phone"] = str(so_dict["phone"]) + + return JsonResponse(so_dict) + else: + return JsonResponse({"error": "Senior Official not found"}, status=404)