Merge branch 'main' into za/2520-admin-portfolio-view

This commit is contained in:
zandercymatics 2024-08-14 12:59:11 -06:00
commit 5afad6b91d
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
11 changed files with 337 additions and 4 deletions

View file

@ -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)
}
}
})();

View file

@ -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) {

View file

@ -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/",

View file

@ -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)

View file

@ -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 field_sets %}
{% for fieldset in adminform %}
{% comment %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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")

View file

@ -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)

View file

@ -8,3 +8,4 @@ from .permission_views import (
DomainInvitationPermissionDeleteView,
DomainRequestWizardPermissionView,
)
from .api_views import get_senior_official_from_federal_agency_json

View file

@ -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)