Merge branch 'main' into za/2402-design-review

This commit is contained in:
zandercymatics 2024-09-27 13:23:24 -06:00
commit ef12c5e93e
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
11 changed files with 565 additions and 400 deletions

View file

@ -1,8 +1,6 @@
from datetime import date from datetime import date
import logging import logging
import copy import copy
import json
from django.template.loader import get_template
from django import forms from django import forms
from django.db.models import Value, CharField, Q from django.db.models import Value, CharField, Q
from django.db.models.functions import Concat, Coalesce from django.db.models.functions import Concat, Coalesce
@ -10,7 +8,7 @@ from django.http import HttpResponseRedirect
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField from django_fsm import get_available_FIELD_transitions, FSMField
from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from waffle.decorators import flag_is_active from waffle.decorators import flag_is_active
from django.contrib import admin, messages from django.contrib import admin, messages
@ -22,6 +20,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from waffle.admin import FlagAdmin from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch from waffle.models import Sample, Switch
from registrar.utility.admin_helpers import get_all_action_needed_reason_emails, get_action_needed_reason_default_email
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
@ -1902,9 +1901,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Set the action_needed_reason_email to the default if nothing exists. # Set the action_needed_reason_email to the default if nothing exists.
# Since this check occurs after save, if the user enters a value then we won't update. # Since this check occurs after save, if the user enters a value then we won't update.
default_email = self._get_action_needed_reason_default_email(obj, obj.action_needed_reason) default_email = get_action_needed_reason_default_email(request, obj, obj.action_needed_reason)
if obj.action_needed_reason_email: if obj.action_needed_reason_email:
emails = self.get_all_action_needed_reason_emails(obj) emails = get_all_action_needed_reason_emails(request, obj)
is_custom_email = obj.action_needed_reason_email not in emails.values() is_custom_email = obj.action_needed_reason_email not in emails.values()
if not is_custom_email: if not is_custom_email:
obj.action_needed_reason_email = default_email obj.action_needed_reason_email = default_email
@ -2134,8 +2133,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Initialize extra_context and add filtered entries # Initialize extra_context and add filtered entries
extra_context = extra_context or {} extra_context = extra_context or {}
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
emails = self.get_all_action_needed_reason_emails(obj)
extra_context["action_needed_reason_emails"] = json.dumps(emails)
# Denote if an action needed email was sent or not # Denote if an action needed email was sent or not
email_sent = request.session.get("action_needed_email_sent", False) email_sent = request.session.get("action_needed_email_sent", False)
@ -2146,39 +2143,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# Call the superclass method with updated extra_context # Call the superclass method with updated extra_context
return super().change_view(request, object_id, form_url, extra_context) return super().change_view(request, object_id, form_url, extra_context)
def get_all_action_needed_reason_emails(self, domain_request):
"""Returns a json 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] = self._get_action_needed_reason_default_email(
domain_request, action_needed_reason.value
)
return emails
def _get_action_needed_reason_default_email(self, 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
def process_log_entry(self, log_entry): def process_log_entry(self, log_entry):
"""Process a log entry and return filtered entry dictionary if applicable.""" """Process a log entry and return filtered entry dictionary if applicable."""
changes = log_entry.changes changes = log_entry.changes
@ -2289,10 +2253,58 @@ class DomainInformationInline(admin.StackedInline):
template = "django/admin/includes/domain_info_inline_stacked.html" template = "django/admin/includes/domain_info_inline_stacked.html"
model = models.DomainInformation model = models.DomainInformation
fieldsets = DomainInformationAdmin.fieldsets fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
readonly_fields = DomainInformationAdmin.readonly_fields analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
autocomplete_fields = DomainInformationAdmin.autocomplete_fields
def get_domain_managers(self, obj):
user_domain_roles = UserDomainRole.objects.filter(domain=obj.domain)
user_ids = user_domain_roles.values_list("user_id", flat=True)
domain_managers = User.objects.filter(id__in=user_ids)
return domain_managers
def get_domain_invitations(self, obj):
domain_invitations = DomainInvitation.objects.filter(
domain=obj.domain, status=DomainInvitation.DomainInvitationStatus.INVITED
)
return domain_invitations
def domain_managers(self, obj):
"""Get domain managers for the domain, unpack and return an HTML block."""
domain_managers = self.get_domain_managers(obj)
if not domain_managers:
return "No domain managers found."
domain_manager_details = "<table><thead><tr><th>UID</th><th>Name</th><th>Email</th></tr></thead><tbody>"
for domain_manager in domain_managers:
full_name = domain_manager.get_formatted_name()
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
domain_manager_details += "<tr>"
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
domain_manager_details += f"<td>{escape(full_name)}</td>"
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
domain_manager_details += "</tr>"
domain_manager_details += "</tbody></table>"
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 = "<table><thead><tr><th>Email</th><th>Status</th>" + "</tr></thead><tbody>"
for domain_invitation in domain_invitations:
domain_invitation_details += "<tr>"
domain_invitation_details += f"<td>{escape(domain_invitation.email)}</td>"
domain_invitation_details += f"<td>{escape(domain_invitation.status.capitalize())}</td>"
domain_invitation_details += "</tr>"
domain_invitation_details += "</tbody></table>"
return format_html(domain_invitation_details)
invited_domain_managers.short_description = "Invited domain managers" # type: ignore
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
"""Custom has_change_permission override so that we can specify that """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) return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_readonly_fields(self, request, obj=None): 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 # 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. # 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): def get_fieldsets(self, request, obj=None):
# Grab fieldsets from DomainInformationAdmin so that it handles all logic # Grab fieldsets from DomainInformationAdmin so that it handles all logic
# for permission-based field visibility. # 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): for index, (title, f) in enumerate(modified_fieldsets):
if title == ".gov domain": if title == ".gov domain":
del modified_fieldsets[index] # remove .gov domain from fieldset
break 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 return modified_fieldsets
@ -2405,13 +2440,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
fieldsets = ( fieldsets = (
( (
None, 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): def generic_org_type(self, obj):
return obj.domain_info.get_generic_org_type_display() 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 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): def custom_election_board(self, obj):
domain_info = getattr(obj, "domain_info", None) domain_info = getattr(obj, "domain_info", None)
if domain_info: if domain_info:
@ -2458,7 +2512,15 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by domain name." search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html" 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 # Table ordering
ordering = ["name"] ordering = ["name"]

View file

@ -504,167 +504,111 @@ function initializeWidgetOnList(list, parentId) {
/** An IIFE that hooks to the show/hide button underneath action needed reason. /** An IIFE that hooks to the show/hide button underneath action needed reason.
* This shows the auto generated email on action needed reason. * This shows the auto generated email on action needed reason.
*/ */
(function () { document.addEventListener('DOMContentLoaded', function() {
// Since this is an iife, these vars will be removed from memory afterwards const dropdown = document.getElementById("id_action_needed_reason");
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason"); const textarea = document.getElementById("id_action_needed_reason_email")
const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
// Placeholder text (for certain "action needed" reasons that do not involve e=mails) const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
var placeholderText = document.querySelector("#action-needed-reason-email-placeholder-text") 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 // We will use the const to control the modal
var actionNeededEmail = document.querySelector("#id_action_needed_reason_email") let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
var actionNeededEmailReadonly = document.querySelector("#action-needed-reason-email-readonly") // We will use the function to control the label and help
var actionNeededEmailReadonlyTextarea = document.querySelector("#action-needed-reason-email-readonly-textarea") function isEmailAlreadySent() {
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
// 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)
});
} }
function checkEmailAlreadySent() if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
{ const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;
lastEmailSent = lastSentEmailText.value.replace(/\s+/g, '')
currentEmailInTextArea = actionNeededEmail.value.replace(/\s+/g, '')
return lastEmailSent === currentEmailInTextArea
}
// Shows a readonly preview of the email with updated messaging to indicate this email was sent function updateUserInterface(reason) {
function showEmailAlreadySentView() 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
hideElement(actionNeededEmailHeader) formLabel.innerHTML = "Email:";
showElement(actionNeededEmailHeaderOnSave) textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
actionNeededEmailFooter.innerHTML = "This email has been sent to the creator of this request"; showElement(textareaPlaceholder);
} hideElement(directEditButton);
hideElement(modalTrigger);
// Shows a readonly preview of the email with updated messaging to indicate this email was sent hideElement(textarea);
function hideEmailAlreadySentView() } 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
showElement(actionNeededEmailHeader) formLabel.innerHTML = "Email:";
hideElement(actionNeededEmailHeaderOnSave) textareaPlaceholder.innerHTML = "No email will be sent";
actionNeededEmailFooter.innerHTML = "This email will be sent to the creator of this request after saving"; showElement(textareaPlaceholder);
} hideElement(directEditButton);
hideElement(modalTrigger);
// Shows either a preview of the email or some text describing no email will be sent. hideElement(textarea);
// 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();
}
}
} else { } else {
// Hide email preview and show this text instead // A triggering selection is selected, all hands on board:
showPlaceholderText("Select an action needed reason to see email"); 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 // Initialize UI
function showEmail(canEdit) updateUserInterface(dropdown.value);
{
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)
}
// Hides preview of action needed email and instead displays the given text (innerHTML) dropdown.addEventListener("change", function() {
function showPlaceholderText(innerHTML) const reason = dropdown.value;
{ // Update the UI
hideElement(actionNeededEmail.parentElement) updateUserInterface(reason);
hideElement(actionNeededEmailReadonly) if (reason && reason !== "other") {
hideElement(actionNeededEmailFooter) // 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) /** An IIFE for copy summary button (appears in DomainRegistry models)

View file

@ -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 { .text-faded {
color: #{$dhs-gray-60}; color: #{$dhs-gray-60};
} }

View file

@ -28,6 +28,7 @@ from registrar.views.transfer_user import TransferUserView
from registrar.views.utility.api_views import ( from registrar.views.utility.api_views import (
get_senior_official_from_federal_agency_json, get_senior_official_from_federal_agency_json,
get_federal_and_portfolio_types_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.domains_json import get_domains_json
from registrar.views.utility import always_404 from registrar.views.utility import always_404
@ -153,6 +154,11 @@ urlpatterns = [
get_federal_and_portfolio_types_from_federal_agency_json, get_federal_and_portfolio_types_from_federal_agency_json,
name="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("admin/", admin.site.urls),
path( path(
"reports/export_data_type_user/", "reports/export_data_type_user/",

View file

@ -8,6 +8,8 @@
{# Store the current object id so we can access it easier #} {# Store the current object id so we can access it easier #}
<input id="domain_request_id" class="display-none" value="{{original.id}}" /> <input id="domain_request_id" class="display-none" value="{{original.id}}" />
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/> <input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
{% url 'get-action-needed-email-for-user-json' as url %}
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
{% for fieldset in adminform %} {% for fieldset in adminform %}
{% comment %} {% comment %}
TODO: this will eventually need to be changed to something like this TODO: this will eventually need to be changed to something like this

View file

@ -66,24 +66,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
No changelog to display. No changelog to display.
</div> </div>
{% endif %} {% endif %}
{% elif field.field.name == "action_needed_reason_email" %}
<div class="readonly textarea-wrapper">
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
<label class="max-full" for="action_needed_reason_email_view_more">
<strong>Sent to creator</strong>
</label>
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
{{ original_object.action_needed_reason_email }}
</textarea>
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
</div>
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1">
<span>Show details</span>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
</svg>
</button>
</div>
{% elif field.field.name == "other_contacts" %} {% elif field.field.name == "other_contacts" %}
{% if all_contacts.count > 2 %} {% if all_contacts.count > 2 %}
<div class="readonly"> <div class="readonly">
@ -137,6 +119,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
</div> </div>
{% elif field.field.name == "display_admins" or field.field.name == "domain_managers" or field.field.namd == "invited_domain_managers" %}
<div class="readonly">{{ field.contents|safe }}</div>
{% elif field.field.name == "display_members" %}
<div class="readonly">
{% if display_members_summary %}
{{ display_members_summary }}
{% else %}
<p>No additional members found.</p>
{% endif %}
</div>
{% else %} {% else %}
<div class="readonly">{{ field.contents }}</div> <div class="readonly">{{ field.contents }}</div>
{% endif %} {% endif %}
@ -145,131 +137,102 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% block field_other %} {% block field_other %}
{% if field.field.name == "action_needed_reason_email" %} {% if field.field.name == "action_needed_reason_email" %}
<div>
<div id="action-needed-reason-email-placeholder-text" class="margin-top-05 text-faded"> <div class="margin-top-05 text-faded field-action_needed_reason_email__placeholder">
- &ndash;
</div> </div>
<div>
<div id="action-needed-reason-email-readonly" class="display-none usa-summary-box_admin padding-top-0 margin-top-0"> {{ field.field }}
<div class="flex-container">
<div class="margin-top-05"> <button
<p class="{% if action_needed_email_sent %}display-none{% endif %}" id="action-needed-email-header"><b>Auto-generated email that will be sent to the creator</b></p> aria-label="Edit email in textarea"
<p class="{% if not action_needed_email_sent %}display-none{% endif %}" id="action-needed-email-header-email-sent"> type="button"
<svg class="usa-icon text-green" aria-hidden="true" focusable="false" role="img"> class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline field-action_needed_reason_email__edit flex-align-self-start"
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use> ><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
</svg> >
<b>Email sent to the creator</b> <a
href="#email-already-sent-modal"
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 field-action_needed_reason_email__modal-trigger flex-align-self-start"
aria-controls="email-already-sent-modal"
data-open-modal
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
>
<div
class="usa-modal"
id="email-already-sent-modal"
aria-labelledby="Are you sure you want to edit this email?"
aria-describedby="The creator of this request already received an email"
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading">
Are you sure you want to edit this email?
</h2>
<div class="usa-prose">
<p>
The creator of this request already received an email for this status/reason:
</p>
<ul>
<li class="font-body-sm">Status: <b>Action needed</b></li>
<li class="font-body-sm">Reason: <b>{{ original_object.get_action_needed_reason_display }}</b></li>
</ul>
<p>
If you edit this email's text, <b>the system will send another email</b> to
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
</p> </p>
</div> </div>
<div class="vertical-separator margin-top-1 margin-bottom-1"></div>
<a
href="#email-already-sent-modal"
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1"
aria-controls="email-already-sent-modal"
data-open-modal
>Edit email</a
>
</div>
<div
class="usa-modal"
id="email-already-sent-modal"
aria-labelledby="Are you sure you want to edit this email?"
aria-describedby="The creator of this request already received an email"
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
Are you sure you want to edit this email?
</h2>
<div class="usa-prose">
<p>
The creator of this request already received an email for this status/reason:
</p>
<ul>
<li class="font-body-sm">Status: <b>Action needed</b></li>
<li class="font-body-sm">Reason: <b>{{ original_object.get_action_needed_reason_display }}</b></li>
</ul>
<p>
If you edit this email's text, <b>the system will send another email</b> to
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
</p>
</div>
<div class="usa-modal__footer"> <div class="usa-modal__footer">
<ul class="usa-button-group"> <ul class="usa-button-group">
<li class="usa-button-group__item"> <li class="usa-button-group__item">
<button <button
type="submit" type="submit"
class="usa-button" class="usa-button"
id="email-already-sent-modal_continue-editing-button" id="confirm-edit-email"
data-close-modal data-close-modal
> >
Yes, continue editing Yes, continue editing
</button> </button>
</li> </li>
<li class="usa-button-group__item"> <li class="usa-button-group__item">
<button <button
type="button" type="button"
class="usa-button usa-button--unstyled padding-105 text-center" class="usa-button usa-button--unstyled padding-105 text-center"
name="_cancel_edit_email" name="_cancel_edit_email"
data-close-modal data-close-modal
> >
Cancel Cancel
</button> </button>
</li> </li>
</ul> </ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
</button>
</div> </div>
</div> </div>
<label class="sr-only" for="action-needed-reason-email-readonly-textarea">Email:</label> <button
<textarea cols="40" rows="10" class="vLargeTextField" id="action-needed-reason-email-readonly-textarea" readonly>{{ field.field.value|striptags }} type="button"
</textarea> class="usa-button usa-modal__close"
</div> aria-label="Close this window"
<div> data-close-modal
{{ field.field }} >
<input id="action-needed-email-sent" class="display-none" value="{{action_needed_email_sent}}"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<input id="action-needed-email-last-sent-text" class="display-none" value="{{original_object.action_needed_reason_email}}"> <use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
</button>
</div> </div>
</div> </div>
<span id="action-needed-email-footer" class="help">
{% if not action_needed_email_sent %} {% if original_object.action_needed_reason_email %}
This email will be sent to the creator of this request after saving <input id="last-sent-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
{% else %} {% else %}
This email has been sent to the creator of this request <input id="last-sent-email-content" class="display-none" value="None">
{% endif %} {% endif %}
</span>
</div>
{% else %} {% else %}
{{ field.field }} {{ field.field }}
{% endif %} {% endif %}
{% endblock field_other %} {% endblock field_other %}
{% block after_help_text %} {% block after_help_text %}
{% if field.field.name == "action_needed_reason_email" %} {% if field.field.name == "creator" %}
{% 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 %}
<script id="action-needed-emails-data" type="application/json">
{{ action_needed_reason_emails|safe }}
</script>
{% endif %}
{% elif field.field.name == "creator" %}
<div class="flex-container tablet:margin-top-2"> <div class="flex-container tablet:margin-top-2">
<label aria-label="Creator contact details"></label> <label aria-label="Creator contact details"></label>
{% 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%} {% 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%}

View file

@ -167,12 +167,6 @@ class TestDomainAdminAsStaff(MockEppLib):
expected_organization_name = "MonkeySeeMonkeyDo" expected_organization_name = "MonkeySeeMonkeyDo"
self.assertContains(response, expected_organization_name) 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 @less_console_noise_decorator
def test_deletion_is_successful(self): def test_deletion_is_successful(self):
""" """
@ -227,9 +221,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
# clean up data within this test
domain.delete()
@less_console_noise_decorator @less_console_noise_decorator
def test_deletion_ready_fsm_failure(self): def test_deletion_ready_fsm_failure(self):
""" """
@ -269,9 +260,6 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.READY) self.assertEqual(domain.state, Domain.State.READY)
# delete data created in this test
domain.delete()
@less_console_noise_decorator @less_console_noise_decorator
def test_analyst_deletes_domain_idempotent(self): def test_analyst_deletes_domain_idempotent(self):
""" """
@ -330,8 +318,130 @@ class TestDomainAdminAsStaff(MockEppLib):
) )
self.assertEqual(domain.state, Domain.State.DELETED) 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'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
domain_managers,
)
self.assertIn("Gerald Meoward", domain_managers)
self.assertIn("meoward@gov.gov", domain_managers)
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', 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'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
domain_managers,
)
self.assertIn("Gerald Meoward", domain_managers)
self.assertIn("meoward@gov.gov", domain_managers)
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', domain_managers)
self.assertIn("Arnold Poopy", domain_managers)
self.assertIn("poopy@gov.gov", domain_managers)
class TestDomainAdminWithClient(TestCase): class TestDomainAdminWithClient(TestCase):
@ -415,17 +525,6 @@ class TestDomainAdminWithClient(TestCase):
self.assertContains(response, domain.name) self.assertContains(response, domain.name)
# Check that the fields have the right values. # 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 == # # == Check for the senior_official == #
self.assertContains(response, "testy@town.com") self.assertContains(response, "testy@town.com")
@ -435,11 +534,6 @@ class TestDomainAdminWithClient(TestCase):
# Includes things like readonly fields # Includes things like readonly fields
self.assertContains(response, "Testy Tester") 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 # Test for the copy link
self.assertContains(response, "button--clipboard") self.assertContains(response, "button--clipboard")

View file

@ -872,23 +872,6 @@ class TestDomainRequestAdmin(MockEppLib):
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED) self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3) 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) @override_settings(IS_PRODUCTION=True)
@less_console_noise_decorator @less_console_noise_decorator
def test_save_model_sends_submitted_email_with_bcc_on_prod(self): def test_save_model_sends_submitted_email_with_bcc_on_prod(self):

View file

@ -1,8 +1,8 @@
from django.urls import reverse from django.urls import reverse
from django.test import TestCase, Client 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 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 api.tests.common import less_console_noise_decorator
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
@ -108,3 +108,71 @@ class GetFederalPortfolioTypeJsonTest(TestCase):
self.client.login(username="testuser", password=p) self.client.login(username="testuser", password=p)
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"}) response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
self.assertEqual(response.status_code, 302) 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)

View file

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

View file

@ -1,10 +1,10 @@
import logging import logging
from django.http import JsonResponse from django.http import JsonResponse
from django.forms.models import model_to_dict 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.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_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.models.portfolio import Portfolio
from registrar.utility.constants import BranchChoices 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) 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)