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

Ticket #2484: Dynamic portfolio fields #2548

Merged
merged 16 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
116 changes: 115 additions & 1 deletion src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -511,11 +511,21 @@ function initializeWidgetOnList(list, parentId) {
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
var actionNeededEmail = document.querySelector("#id_action_needed_reason_email");
var readonlyView = document.querySelector("#action-needed-reason-email-readonly");
if(!actionNeededEmail || !actionNeededReasonDropdown) {
return;
}

let emailWasSent = document.getElementById("action-needed-email-sent");
if (!emailWasSent) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 534 should be handling this and !actionNeededEmail || !actionNeededReasonDropdown cases

Copy link
Contributor Author

@zandercymatics zandercymatics Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these because this script was failing on the portfolio page, causing script execution to stop (on textContent because the query returned none). That said I can simplify these checks to just check on if action-needed-emails-data exists or not before trying to do .textContent

return;
}

let actionNeededEmailData = document.getElementById('action-needed-emails-data').textContent;
let actionNeededEmailsJson = JSON.parse(actionNeededEmailData);
if(!actionNeededEmailData) {
return;
}

let actionNeededEmailsJson = JSON.parse(actionNeededEmailData);
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
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 +760,107 @@ function initializeWidgetOnList(list, parentId) {
});
}
})();


/** An IIFE for copy summary button (appears in DomainRegistry models)
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
*/
(function dynamicPortfolioFields(){

document.addEventListener('DOMContentLoaded', function() {

let isPortfolioPage = document.querySelector("#portfolio_form");
if (!isPortfolioPage) {
return;
}

// $ symbolically denotes that this is using jQuery
let $federalAgency = django.jQuery("#id_federal_agency");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Why use django.JQuery instead of getElement or querySelector?

Copy link
Contributor Author

@zandercymatics zandercymatics Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, its because everywhere else in the app federal agency is a select2 (autocomplete field). The portfolio page is still undergoing some work - so its likely that this will be added to that as well. Doing it this way gives us out of the box support so we don't have to alter this script when we do.

For context: you can't do .value on Select2 objects. You can only manipulate them through jQuery, its a quirk of that particular library. Went down that rabbit hole a few tickets ago

let organizationType = document.querySelector("#id_organization_type");
rachidatecs marked this conversation as resolved.
Show resolved Hide resolved
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.querySelector("#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();
if (selectedText !== "Non-Federal Agency" && selectedText) {
if (organizationType.value !== "federal") {
organizationType.value = "federal";
}
// Set the SO field
}else if (selectedText === "Non-Federal Agency" && organizationType.value === "federal") {
organizationType.value = "";
// Set the SO field
}

// There isn't a senior official associated with null records and non federal agencies
if (!selectedText || selectedText === "Non-Federal Agency") {
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// 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.querySelector("#senior_official_from_agency_json_url").value;
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
.then(response => response.json())
.then(data => {
if (data.error) {
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()){
console.error("Could not assign current Senior Official: no values found.")
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
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)
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
}
}
})();
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 @@ -123,6 +124,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
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 easlier access {% endcomment %}
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
{% url 'get-senior-official-from-federal-agency-json' as url %}
<input id="senior_official_from_agency_json_url" class="display-none" value="{{url}}" />
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
{{ block.super }}
{% endblock content %}

{% block after_related_objects %}
<div class="module aligned padding-3">
<h2>Associated groups and suborganizations</h2>
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):
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved
"""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")
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
30 changes: 30 additions & 0 deletions src/registrar/views/utility/api_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant with the superuser and analyst checks you're doing below. That's not a bad thing, but I'm wondering why you bothered with the decorator?

Copy link
Contributor Author

@zandercymatics zandercymatics Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decorator redirects you to the admin login page and basically does the default django checks for admin perms. Tiny QOL thing, otherwise you do get blocked but you can technically still """access""" the endpoint

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)
zandercymatics marked this conversation as resolved.
Show resolved Hide resolved

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)
return JsonResponse(so_dict)
else:
return JsonResponse({"error": "Senior Official not found"}, status=404)
Loading