Skip to content

Commit

Permalink
Merge pull request #2548 from cisagov/meoward/2484-dynamic-portfolio-…
Browse files Browse the repository at this point in the history
…fields

Ticket #2484: Dynamic portfolio fields
  • Loading branch information
zandercymatics authored Aug 14, 2024
2 parents 23ec004 + 67cd368 commit 9a55ade
Show file tree
Hide file tree
Showing 11 changed files with 337 additions and 4 deletions.
128 changes: 126 additions & 2 deletions src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}
}
})();
25 changes: 23 additions & 2 deletions src/registrar/assets/js/get-gov.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`

Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/registrar/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

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
Expand Down Expand Up @@ -128,6 +129,11 @@
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/",
Expand Down
9 changes: 9 additions & 0 deletions src/registrar/models/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,12 @@ class Portfolio(TimeStampedModel):

def __str__(self) -> str:
return f"{self.organization_name}"

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)
Original file line number Diff line number Diff line change
@@ -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 %}
<input id="senior_official_from_agency_json_url" class="display-none" value="{{url}}" />
{{ block.super }}
{% endblock content %}

{% block after_related_objects %}
<div class="module aligned padding-3">
<h2>Associated groups and suborganizations</h2>
Expand Down
3 changes: 3 additions & 0 deletions src/registrar/templates/includes/domain_requests_table.html
Original file line number Diff line number Diff line change
@@ -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 %}
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
<section class="section--outlined domain-requests" id="domain-requests">
<div class="grid-row">
{% if not has_domain_requests_portfolio_permission %}
Expand Down
5 changes: 5 additions & 0 deletions src/registrar/templates/includes/domains_table.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{% load static %}



{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domains_json' as url %}
<span id="get_domains_json_url" class="display-none">{{url}}</span>
<section class="section--outlined domains{% if not has_domains_portfolio_permission %} margin-top-0{% endif %}" id="domains">
<div class="section--outlined__header margin-bottom-3 {% if not has_domains_portfolio_permission %} section--outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
{% if not has_domains_portfolio_permission %}
Expand Down
67 changes: 67 additions & 0 deletions src/registrar/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -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")
54 changes: 54 additions & 0 deletions src/registrar/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2266,3 +2266,57 @@ def test_form_complete(self):
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="[email protected]", email="[email protected]", 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)
1 change: 1 addition & 0 deletions src/registrar/views/utility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
DomainInvitationPermissionDeleteView,
DomainRequestWizardPermissionView,
)
from .api_views import get_senior_official_from_federal_agency_json
Loading

0 comments on commit 9a55ade

Please sign in to comment.