"
+ return format_html(domain_manager_details)
+
+ domain_managers.short_description = "Domain managers" # type: ignore
+
+ def invited_domain_managers(self, obj):
+ """Get emails which have been invited to the domain, unpack and return an HTML block."""
+ domain_invitations = self.get_domain_invitations(obj)
+ if not domain_invitations:
+ return "No invited domain managers found."
+
+ domain_invitation_details = "
Email
Status
" + "
"
+ for domain_invitation in domain_invitations:
+ domain_invitation_details += "
"
+ domain_invitation_details += f"
{escape(domain_invitation.email)}
"
+ domain_invitation_details += f"
{escape(domain_invitation.status.capitalize())}
"
+ domain_invitation_details += "
"
+ domain_invitation_details += "
"
+ return format_html(domain_invitation_details)
+
+ invited_domain_managers.short_description = "Invited domain managers" # type: ignore
def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that
@@ -2332,7 +2344,9 @@ class DomainInformationInline(admin.StackedInline):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None):
- return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
+ readonly_fields = copy.deepcopy(DomainInformationAdmin.get_readonly_fields(self, request, obj=None))
+ readonly_fields.extend(["domain_managers", "invited_domain_managers"]) # type: ignore
+ return readonly_fields
# Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets
# since that has all the logic for excluding certain fields according to user permissions.
@@ -2341,13 +2355,34 @@ class DomainInformationInline(admin.StackedInline):
def get_fieldsets(self, request, obj=None):
# Grab fieldsets from DomainInformationAdmin so that it handles all logic
# for permission-based field visibility.
- modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None)
+ modified_fieldsets = copy.deepcopy(DomainInformationAdmin.get_fieldsets(self, request, obj=None))
- # remove .gov domain from fieldset
+ # Modify fieldset sections in place
+ for index, (title, options) in enumerate(modified_fieldsets):
+ if title is None:
+ options["fields"] = [
+ field for field in options["fields"] if field not in ["creator", "domain_request", "notes"]
+ ]
+ elif title == "Contacts":
+ options["fields"] = [
+ field
+ for field in options["fields"]
+ if field not in ["other_contacts", "no_other_contacts_rationale"]
+ ]
+ options["fields"].extend(["domain_managers", "invited_domain_managers"]) # type: ignore
+ elif title == "Background info":
+ # move domain request and notes to background
+ options["fields"].extend(["domain_request", "notes"]) # type: ignore
+
+ # Remove or remove fieldset sections
for index, (title, f) in enumerate(modified_fieldsets):
if title == ".gov domain":
- del modified_fieldsets[index]
- break
+ # remove .gov domain from fieldset
+ modified_fieldsets.pop(index)
+ elif title == "Background info":
+ # move Background info to the bottom of the list
+ fieldsets_to_move = modified_fieldsets.pop(index)
+ modified_fieldsets.append(fieldsets_to_move)
return modified_fieldsets
@@ -2405,13 +2440,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
fieldsets = (
(
None,
- {"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
+ {"fields": ["state", "expiration_date", "first_ready", "deleted", "dnssecdata", "nameservers"]},
),
)
- # this ordering effects the ordering of results in autocomplete_fields for domain
- ordering = ["name"]
-
def generic_org_type(self, obj):
return obj.domain_info.get_generic_org_type_display()
@@ -2432,6 +2464,28 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
+ def dnssecdata(self, obj):
+ return "Yes" if obj.dnssecdata else "No"
+
+ dnssecdata.short_description = "DNSSEC enabled" # type: ignore
+
+ # Custom method to display formatted nameservers
+ def nameservers(self, obj):
+ if not obj.nameservers:
+ return "No nameservers"
+
+ formatted_nameservers = []
+ for server, ip_list in obj.nameservers:
+ server_display = str(server)
+ if ip_list:
+ server_display += f" [{', '.join(ip_list)}]"
+ formatted_nameservers.append(server_display)
+
+ # Join the formatted strings with line breaks
+ return "\n".join(formatted_nameservers)
+
+ nameservers.short_description = "Name servers" # type: ignore
+
def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None)
if domain_info:
@@ -2458,7 +2512,15 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_fields = ["name"]
search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html"
- readonly_fields = ("state", "expiration_date", "first_ready", "deleted", "federal_agency")
+ readonly_fields = (
+ "state",
+ "expiration_date",
+ "first_ready",
+ "deleted",
+ "federal_agency",
+ "dnssecdata",
+ "nameservers",
+ )
# Table ordering
ordering = ["name"]
diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js
index 25e35b73b..73f3dded1 100644
--- a/src/registrar/assets/js/get-gov-admin.js
+++ b/src/registrar/assets/js/get-gov-admin.js
@@ -504,167 +504,111 @@ function initializeWidgetOnList(list, parentId) {
/** An IIFE that hooks to the show/hide button underneath action needed reason.
* This shows the auto generated email on action needed reason.
*/
-(function () {
- // Since this is an iife, these vars will be removed from memory afterwards
- var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
-
- // Placeholder text (for certain "action needed" reasons that do not involve e=mails)
- var placeholderText = document.querySelector("#action-needed-reason-email-placeholder-text")
+document.addEventListener('DOMContentLoaded', function() {
+ const dropdown = document.getElementById("id_action_needed_reason");
+ const textarea = document.getElementById("id_action_needed_reason_email")
+ const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
+ const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
+ const directEditButton = document.querySelector('.field-action_needed_reason_email__edit');
+ const modalTrigger = document.querySelector('.field-action_needed_reason_email__modal-trigger');
+ const modalConfirm = document.getElementById('confirm-edit-email');
+ const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]');
+ let lastSentEmailContent = document.getElementById("last-sent-email-content");
+ const initialDropdownValue = dropdown ? dropdown.value : null;
+ const initialEmailValue = textarea.value;
- // E-mail divs and textarea components
- var actionNeededEmail = document.querySelector("#id_action_needed_reason_email")
- var actionNeededEmailReadonly = document.querySelector("#action-needed-reason-email-readonly")
- var actionNeededEmailReadonlyTextarea = document.querySelector("#action-needed-reason-email-readonly-textarea")
-
- // Edit e-mail modal (and its confirmation button)
- var confirmEditEmailButton = document.querySelector("#email-already-sent-modal_continue-editing-button")
-
- // Headers and footers (which change depending on if the e-mail was sent or not)
- var actionNeededEmailHeader = document.querySelector("#action-needed-email-header")
- var actionNeededEmailHeaderOnSave = document.querySelector("#action-needed-email-header-email-sent")
- var actionNeededEmailFooter = document.querySelector("#action-needed-email-footer")
-
- let emailWasSent = document.getElementById("action-needed-email-sent");
- let lastSentEmailText = document.getElementById("action-needed-email-last-sent-text");
-
- // Get the list of e-mails associated with each action-needed dropdown value
- 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;
- const oldEmailValue = actionNeededEmailData ? actionNeededEmailData.value : null;
-
- if(actionNeededReasonDropdown && actionNeededEmail && domainRequestId) {
- // Add a change listener to dom load
- document.addEventListener('DOMContentLoaded', function() {
- let reason = actionNeededReasonDropdown.value;
-
- // Handle the session boolean (to enable/disable editing)
- if (emailWasSent && emailWasSent.value === "True") {
- // An email was sent out - store that information in a session variable
- addOrRemoveSessionBoolean(emailSentSessionVariableName, add=true);
- }
-
- // Show an editable email field or a readonly one
- updateActionNeededEmailDisplay(reason)
- });
-
- // editEmailButton.addEventListener("click", function() {
- // if (!checkEmailAlreadySent()) {
- // showEmail(canEdit=true)
- // }
- // });
-
- confirmEditEmailButton.addEventListener("click", function() {
- // Show editable view
- showEmail(canEdit=true)
- });
-
-
- // Add a change listener to the action needed reason dropdown
- actionNeededReasonDropdown.addEventListener("change", function() {
- let reason = actionNeededReasonDropdown.value;
- let emailBody = reason in actionNeededEmailsJson ? actionNeededEmailsJson[reason] : null;
-
- if (reason && emailBody) {
- // Reset the session object on change since change refreshes the email content.
- if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) {
- // Replace the email content
- actionNeededEmail.value = emailBody;
- actionNeededEmailReadonlyTextarea.value = emailBody;
- hideEmailAlreadySentView();
- }
- }
-
- // Show either a preview of the email or some text describing no email will be sent
- updateActionNeededEmailDisplay(reason)
- });
+ // We will use the const to control the modal
+ let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
+ // We will use the function to control the label and help
+ function isEmailAlreadySent() {
+ return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
}
- function checkEmailAlreadySent()
- {
- lastEmailSent = lastSentEmailText.value.replace(/\s+/g, '')
- currentEmailInTextArea = actionNeededEmail.value.replace(/\s+/g, '')
- return lastEmailSent === currentEmailInTextArea
- }
+ if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
+ const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;
- // Shows a readonly preview of the email with updated messaging to indicate this email was sent
- function showEmailAlreadySentView()
- {
- hideElement(actionNeededEmailHeader)
- showElement(actionNeededEmailHeaderOnSave)
- actionNeededEmailFooter.innerHTML = "This email has been sent to the creator of this request";
- }
-
- // Shows a readonly preview of the email with updated messaging to indicate this email was sent
- function hideEmailAlreadySentView()
- {
- showElement(actionNeededEmailHeader)
- hideElement(actionNeededEmailHeaderOnSave)
- actionNeededEmailFooter.innerHTML = "This email will be sent to the creator of this request after saving";
- }
-
- // Shows either a preview of the email or some text describing no email will be sent.
- // If the email doesn't exist or if we're of reason "other", display that no email was sent.
- function updateActionNeededEmailDisplay(reason) {
- hideElement(actionNeededEmail.parentElement)
-
- if (reason) {
- if (reason === "other") {
- // Hide email preview and show this text instead
- showPlaceholderText("No email will be sent");
- }
- else {
- // Always show readonly view of email to start
- showEmail(canEdit=false)
- if(checkEmailAlreadySent())
- {
- showEmailAlreadySentView();
- }
- }
+ function updateUserInterface(reason) {
+ if (!reason) {
+ // No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
+ formLabel.innerHTML = "Email:";
+ textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
+ showElement(textareaPlaceholder);
+ hideElement(directEditButton);
+ hideElement(modalTrigger);
+ hideElement(textarea);
+ } else if (reason === 'other') {
+ // 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
+ formLabel.innerHTML = "Email:";
+ textareaPlaceholder.innerHTML = "No email will be sent";
+ showElement(textareaPlaceholder);
+ hideElement(directEditButton);
+ hideElement(modalTrigger);
+ hideElement(textarea);
} else {
- // Hide email preview and show this text instead
- showPlaceholderText("Select an action needed reason to see email");
+ // A triggering selection is selected, all hands on board:
+ textarea.setAttribute('readonly', true);
+ showElement(textarea);
+ hideElement(textareaPlaceholder);
+
+ if (isEmailAlreadySentConst) {
+ hideElement(directEditButton);
+ showElement(modalTrigger);
+ } else {
+ showElement(directEditButton);
+ hideElement(modalTrigger);
+ }
+ if (isEmailAlreadySent()) {
+ formLabel.innerHTML = "Email sent to creator:";
+ } else {
+ formLabel.innerHTML = "Email:";
+ }
}
}
- // Shows either a readonly view (canEdit=false) or editable view (canEdit=true) of the action needed email
- function showEmail(canEdit)
- {
- if(!canEdit)
- {
- showElement(actionNeededEmailReadonly)
- hideElement(actionNeededEmail.parentElement)
- }
- else
- {
- hideElement(actionNeededEmailReadonly)
- showElement(actionNeededEmail.parentElement)
- }
- showElement(actionNeededEmailFooter) // this is the same for both views, so it was separated out
- hideElement(placeholderText)
- }
+ // Initialize UI
+ updateUserInterface(dropdown.value);
- // Hides preview of action needed email and instead displays the given text (innerHTML)
- function showPlaceholderText(innerHTML)
- {
- hideElement(actionNeededEmail.parentElement)
- hideElement(actionNeededEmailReadonly)
- hideElement(actionNeededEmailFooter)
+ dropdown.addEventListener("change", function() {
+ const reason = dropdown.value;
+ // Update the UI
+ updateUserInterface(reason);
+ if (reason && reason !== "other") {
+ // If it's not the initial value
+ if (initialDropdownValue !== dropdown.value || initialEmailValue !== textarea.value) {
+ // Replace the email content
+ fetch(`${apiUrl}?reason=${reason}&domain_request_id=${domainRequestId}`)
+ .then(response => {
+ return response.json().then(data => data);
+ })
+ .then(data => {
+ if (data.error) {
+ console.error("Error in AJAX call: " + data.error);
+ }else {
+ textarea.value = data.action_needed_email;
+ }
+ updateUserInterface(reason);
+ })
+ .catch(error => {
+ console.error("Error action needed email: ", error)
+ });
+ }
+ }
- placeholderText.innerHTML = innerHTML;
- showElement(placeholderText)
- }
-})();
+ });
+
+ modalConfirm.addEventListener("click", () => {
+ textarea.removeAttribute('readonly');
+ textarea.focus();
+ hideElement(directEditButton);
+ hideElement(modalTrigger);
+ });
+ directEditButton.addEventListener("click", () => {
+ textarea.removeAttribute('readonly');
+ textarea.focus();
+ hideElement(directEditButton);
+ hideElement(modalTrigger);
+ });
+});
/** An IIFE for copy summary button (appears in DomainRegistry models)
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index 3859b3270..c0d50bac1 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -894,23 +894,6 @@ div.dja__model-description{
}
}
-.vertical-separator {
- min-height: 20px;
- height: 100%;
- width: 1px;
- background-color: #d1d2d2;
- vertical-align: middle
-}
-
-.usa-summary-box_admin {
- color: var(--body-fg);
- border-color: var(--summary-box-border);
- background-color: var(--summary-box-bg);
- min-width: fit-content;
- padding: .5rem;
- border-radius: .25rem;
-}
-
.text-faded {
color: #{$dhs-gray-60};
}
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
index f45770cf6..76c77955f 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -28,6 +28,7 @@ from registrar.views.transfer_user import TransferUserView
from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json,
get_federal_and_portfolio_types_from_federal_agency_json,
+ get_action_needed_email_for_user_json,
)
from registrar.views.domains_json import get_domains_json
from registrar.views.utility import always_404
@@ -153,6 +154,11 @@ urlpatterns = [
get_federal_and_portfolio_types_from_federal_agency_json,
name="get-federal-and-portfolio-types-from-federal-agency-json",
),
+ path(
+ "admin/api/get-action-needed-email-for-user-json/",
+ get_action_needed_email_for_user_json,
+ name="get-action-needed-email-for-user-json",
+ ),
path("admin/", admin.site.urls),
path(
"reports/export_data_type_user/",
diff --git a/src/registrar/templates/django/admin/domain_request_change_form.html b/src/registrar/templates/django/admin/domain_request_change_form.html
index 0396326d9..afdd9e6c2 100644
--- a/src/registrar/templates/django/admin/domain_request_change_form.html
+++ b/src/registrar/templates/django/admin/domain_request_change_form.html
@@ -8,6 +8,8 @@
{# Store the current object id so we can access it easier #}
+ {% url 'get-action-needed-email-for-user-json' as url %}
+
{% for fieldset in adminform %}
{% comment %}
TODO: this will eventually need to be changed to something like this
diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
index e03203f4b..f6ada5166 100644
--- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
+++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html
@@ -66,24 +66,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
No changelog to display.
{% endif %}
- {% elif field.field.name == "action_needed_reason_email" %}
-
{% endif %}
@@ -145,131 +137,102 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% block field_other %}
{% if field.field.name == "action_needed_reason_email" %}
-
-
- -
+
+
+ –
-
-
-
-
-
Auto-generated email that will be sent to the creator
-
-
- Email sent to the creator
+
+ {{ field.field }}
+
+
+ Edit email
+
+
+
+
+ Are you sure you want to edit this email?
+
+
+
+ The creator of this request already received an email for this status/reason:
+
+ If you edit this email's text, the system will send another email to
+ the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
- If you edit this email's text, the system will send another email to
- the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
-
-
-
-
-
+
-
-
-
-
- {{ field.field }}
-
-
+
-
- {% if not action_needed_email_sent %}
- This email will be sent to the creator of this request after saving
+
+ {% if original_object.action_needed_reason_email %}
+
{% else %}
- This email has been sent to the creator of this request
+
{% endif %}
-
-
+
{% else %}
{{ field.field }}
{% endif %}
{% endblock field_other %}
{% block after_help_text %}
- {% if field.field.name == "action_needed_reason_email" %}
- {% comment %}
- Store the action needed reason emails in a json-based dictionary.
- This allows us to change the action_needed_reason_email field dynamically, depending on value.
- The alternative to this is an API endpoint.
-
- Given that we have a limited number of emails, doing it this way makes sense.
- {% endcomment %}
- {% if action_needed_reason_emails %}
-
- {% endif %}
- {% elif field.field.name == "creator" %}
+ {% if field.field.name == "creator" %}
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py
index 49f095a25..a9b94781f 100644
--- a/src/registrar/tests/test_admin_domain.py
+++ b/src/registrar/tests/test_admin_domain.py
@@ -167,12 +167,6 @@ class TestDomainAdminAsStaff(MockEppLib):
expected_organization_name = "MonkeySeeMonkeyDo"
self.assertContains(response, expected_organization_name)
- # clean up this test's data
- domain.delete()
- domain_information.delete()
- _domain_request.delete()
- _creator.delete()
-
@less_console_noise_decorator
def test_deletion_is_successful(self):
"""
@@ -227,9 +221,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.DELETED)
- # clean up data within this test
- domain.delete()
-
@less_console_noise_decorator
def test_deletion_ready_fsm_failure(self):
"""
@@ -269,9 +260,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.READY)
- # delete data created in this test
- domain.delete()
-
@less_console_noise_decorator
def test_analyst_deletes_domain_idempotent(self):
"""
@@ -330,8 +318,130 @@ class TestDomainAdminAsStaff(MockEppLib):
)
self.assertEqual(domain.state, Domain.State.DELETED)
- # delete data created in this test
- domain.delete()
+
+class TestDomainInformationInline(MockEppLib):
+ """Test DomainAdmin class, specifically the DomainInformationInline class, as staff user.
+
+ Notes:
+ all tests share staffuser; do not change staffuser model in tests
+ tests have available staffuser, client, and admin
+ """
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.staffuser = create_user()
+ cls.site = AdminSite()
+ cls.admin = DomainAdmin(model=Domain, admin_site=cls.site)
+ cls.factory = RequestFactory()
+
+ def setUp(self):
+ self.client = Client(HTTP_HOST="localhost:8080")
+ self.client.force_login(self.staffuser)
+ super().setUp()
+
+ def tearDown(self):
+ super().tearDown()
+ Host.objects.all().delete()
+ UserDomainRole.objects.all().delete()
+ Domain.objects.all().delete()
+ DomainInformation.objects.all().delete()
+ DomainRequest.objects.all().delete()
+
+ @classmethod
+ def tearDownClass(cls):
+ User.objects.all().delete()
+ super().tearDownClass()
+
+ @less_console_noise_decorator
+ def test_domain_managers_display(self):
+ """Tests the custom domain managers field"""
+ admin_user_1 = User.objects.create(
+ username="testuser1",
+ first_name="Gerald",
+ last_name="Meoward",
+ email="meoward@gov.gov",
+ )
+
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
+ )
+ domain_request.approve()
+ _domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
+ domain = Domain.objects.filter(domain_info=_domain_info).get()
+
+ UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
+
+ admin_user_2 = User.objects.create(
+ username="testuser2",
+ first_name="Arnold",
+ last_name="Poopy",
+ email="poopy@gov.gov",
+ )
+
+ UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
+
+ # Get the first inline (DomainInformationInline)
+ inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
+
+ # Call the domain_managers method
+ domain_managers = inline_instance.domain_managers(domain.domain_info)
+
+ self.assertIn(
+ f'testuser1',
+ domain_managers,
+ )
+ self.assertIn("Gerald Meoward", domain_managers)
+ self.assertIn("meoward@gov.gov", domain_managers)
+ self.assertIn(f'testuser2', domain_managers)
+ self.assertIn("Arnold Poopy", domain_managers)
+ self.assertIn("poopy@gov.gov", domain_managers)
+
+ @less_console_noise_decorator
+ def test_invited_domain_managers_display(self):
+ """Tests the custom invited domain managers field"""
+ admin_user_1 = User.objects.create(
+ username="testuser1",
+ first_name="Gerald",
+ last_name="Meoward",
+ email="meoward@gov.gov",
+ )
+
+ domain_request = completed_domain_request(
+ status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
+ )
+ domain_request.approve()
+ _domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
+ domain = Domain.objects.filter(domain_info=_domain_info).get()
+
+ # domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
+ UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
+
+ admin_user_2 = User.objects.create(
+ username="testuser2",
+ first_name="Arnold",
+ last_name="Poopy",
+ email="poopy@gov.gov",
+ )
+
+ UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
+
+ # Get the first inline (DomainInformationInline)
+ inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
+
+ # Call the domain_managers method
+ domain_managers = inline_instance.domain_managers(domain.domain_info)
+ # domain_managers = self.admin.get_inlinesdomain_managers(self.domain)
+
+ self.assertIn(
+ f'testuser1',
+ domain_managers,
+ )
+ self.assertIn("Gerald Meoward", domain_managers)
+ self.assertIn("meoward@gov.gov", domain_managers)
+ self.assertIn(f'testuser2', domain_managers)
+ self.assertIn("Arnold Poopy", domain_managers)
+ self.assertIn("poopy@gov.gov", domain_managers)
class TestDomainAdminWithClient(TestCase):
@@ -415,17 +525,6 @@ class TestDomainAdminWithClient(TestCase):
self.assertContains(response, domain.name)
# Check that the fields have the right values.
- # == Check for the creator == #
-
- # Check for the right title, email, and phone number in the response.
- # We only need to check for the end tag
- # (Otherwise this test will fail if we change classes, etc)
- self.assertContains(response, "Treat inspector")
- self.assertContains(response, "meoward.jones@igorville.gov")
- self.assertContains(response, "(555) 123 12345")
-
- # Check for the field itself
- self.assertContains(response, "Meoward Jones")
# == Check for the senior_official == #
self.assertContains(response, "testy@town.com")
@@ -435,11 +534,6 @@ class TestDomainAdminWithClient(TestCase):
# Includes things like readonly fields
self.assertContains(response, "Testy Tester")
- # == Test the other_employees field == #
- self.assertContains(response, "testy2@town.com")
- self.assertContains(response, "Another Tester")
- self.assertContains(response, "(555) 555 5557")
-
# Test for the copy link
self.assertContains(response, "button--clipboard")
diff --git a/src/registrar/tests/test_admin_request.py b/src/registrar/tests/test_admin_request.py
index 28639e69b..a9b073472 100644
--- a/src/registrar/tests/test_admin_request.py
+++ b/src/registrar/tests/test_admin_request.py
@@ -872,23 +872,6 @@ class TestDomainRequestAdmin(MockEppLib):
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
- @less_console_noise_decorator
- def test_model_displays_action_needed_email(self):
- """Tests if the action needed email is visible for Domain Requests"""
-
- _domain_request = completed_domain_request(
- status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
- action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
- )
-
- self.client.force_login(self.staffuser)
- response = self.client.get(
- "/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
- follow=True,
- )
-
- self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS")
-
@override_settings(IS_PRODUCTION=True)
@less_console_noise_decorator
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
diff --git a/src/registrar/tests/test_api.py b/src/registrar/tests/test_api.py
index 7a432bcb8..218c63d4f 100644
--- a/src/registrar/tests/test_api.py
+++ b/src/registrar/tests/test_api.py
@@ -1,8 +1,8 @@
from django.urls import reverse
from django.test import TestCase, Client
-from registrar.models import FederalAgency, SeniorOfficial, User
+from registrar.models import FederalAgency, SeniorOfficial, User, DomainRequest
from django.contrib.auth import get_user_model
-from registrar.tests.common import create_superuser, create_user
+from registrar.tests.common import create_superuser, create_user, completed_domain_request
from api.tests.common import less_console_noise_decorator
from registrar.utility.constants import BranchChoices
@@ -108,3 +108,71 @@ class GetFederalPortfolioTypeJsonTest(TestCase):
self.client.login(username="testuser", password=p)
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
self.assertEqual(response.status_code, 302)
+
+
+class GetActionNeededEmailForUserJsonTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.superuser = create_superuser()
+ self.analyst_user = create_user()
+ self.agency = FederalAgency.objects.create(agency="Test Agency")
+ self.domain_request = completed_domain_request(
+ federal_agency=self.agency,
+ name="test.gov",
+ status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
+ )
+
+ self.api_url = reverse("get-action-needed-email-for-user-json")
+
+ def tearDown(self):
+ DomainRequest.objects.all().delete()
+ User.objects.all().delete()
+ FederalAgency.objects.all().delete()
+
+ @less_console_noise_decorator
+ def test_get_action_needed_email_for_user_json_superuser(self):
+ """Test that a superuser can fetch the action needed email."""
+ self.client.force_login(self.superuser)
+
+ response = self.client.get(
+ self.api_url,
+ {
+ "reason": DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR,
+ "domain_request_id": self.domain_request.id,
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertIn("action_needed_email", data)
+ self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
+
+ @less_console_noise_decorator
+ def test_get_action_needed_email_for_user_json_analyst(self):
+ """Test that an analyst can fetch the action needed email."""
+ self.client.force_login(self.analyst_user)
+
+ response = self.client.get(
+ self.api_url,
+ {
+ "reason": DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL,
+ "domain_request_id": self.domain_request.id,
+ },
+ )
+ self.assertEqual(response.status_code, 200)
+ data = response.json()
+ self.assertIn("action_needed_email", data)
+ self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
+
+ @less_console_noise_decorator
+ def test_get_action_needed_email_for_user_json_regular(self):
+ """Test that a regular user receives a 403 with an error message."""
+ p = "password"
+ self.client.login(username="testuser", password=p)
+ response = self.client.get(
+ self.api_url,
+ {
+ "reason": DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL,
+ "domain_request_id": self.domain_request.id,
+ },
+ )
+ self.assertEqual(response.status_code, 302)
diff --git a/src/registrar/utility/admin_helpers.py b/src/registrar/utility/admin_helpers.py
new file mode 100644
index 000000000..0b99bba13
--- /dev/null
+++ b/src/registrar/utility/admin_helpers.py
@@ -0,0 +1,36 @@
+from registrar.models.domain_request import DomainRequest
+from django.template.loader import get_template
+
+
+def get_all_action_needed_reason_emails(request, domain_request):
+ """Returns a dictionary of every action needed reason and its associated email
+ for this particular domain request."""
+
+ emails = {}
+ for action_needed_reason in domain_request.ActionNeededReasons:
+ # Map the action_needed_reason to its default email
+ emails[action_needed_reason.value] = get_action_needed_reason_default_email(
+ request, domain_request, action_needed_reason.value
+ )
+
+ return emails
+
+
+def get_action_needed_reason_default_email(request, domain_request, action_needed_reason):
+ """Returns the default email associated with the given action needed reason"""
+ if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
+ return None
+
+ recipient = domain_request.creator
+ # Return the context of the rendered views
+ context = {"domain_request": domain_request, "recipient": recipient}
+
+ # Get the email body
+ template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
+
+ email_body_text = get_template(template_path).render(context=context)
+ email_body_text_cleaned = None
+ if email_body_text:
+ email_body_text_cleaned = email_body_text.strip().lstrip("\n")
+
+ return email_body_text_cleaned
diff --git a/src/registrar/views/utility/api_views.py b/src/registrar/views/utility/api_views.py
index 7240cf43f..f9522e2e9 100644
--- a/src/registrar/views/utility/api_views.py
+++ b/src/registrar/views/utility/api_views.py
@@ -1,10 +1,10 @@
import logging
from django.http import JsonResponse
from django.forms.models import model_to_dict
-from registrar.models import FederalAgency, SeniorOfficial
+from registrar.models import FederalAgency, SeniorOfficial, DomainRequest
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
-
+from registrar.utility.admin_helpers import get_all_action_needed_reason_emails
from registrar.models.portfolio import Portfolio
from registrar.utility.constants import BranchChoices
@@ -66,3 +66,27 @@ def get_federal_and_portfolio_types_from_federal_agency_json(request):
}
return JsonResponse(response_data)
+
+
+@login_required
+@staff_member_required
+def get_action_needed_email_for_user_json(request):
+ """Returns a default action needed email for a given user"""
+
+ # 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)
+
+ reason = request.GET.get("reason")
+ domain_request_id = request.GET.get("domain_request_id")
+ if not reason:
+ return JsonResponse({"error": "No reason specified"}, status=404)
+
+ if not domain_request_id:
+ return JsonResponse({"error": "No domain_request_id specified"}, status=404)
+
+ domain_request = DomainRequest.objects.filter(id=domain_request_id).first()
+ emails = get_all_action_needed_reason_emails(request, domain_request)
+ return JsonResponse({"action_needed_email": emails.get(reason)}, status=200)