Merge branch 'main' into za/2597-block-email-sending

This commit is contained in:
zandercymatics 2024-08-27 11:13:59 -06:00
commit c49236e0ec
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
8 changed files with 385 additions and 64 deletions

View file

@ -35,6 +35,7 @@ from django_fsm import TransitionNotAllowed # type: ignore
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.html import escape from django.utils.html import escape
from django.contrib.auth.forms import UserChangeForm, UsernameField from django.contrib.auth.forms import UserChangeForm, UsernameField
from django.contrib.admin.views.main import IGNORED_PARAMS
from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter
from import_export import resources from import_export import resources
from import_export.admin import ImportExportModelAdmin from import_export.admin import ImportExportModelAdmin
@ -224,7 +225,7 @@ class DomainRequestAdminForm(forms.ModelForm):
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False), "other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
} }
labels = { labels = {
"action_needed_reason_email": "Auto-generated email", "action_needed_reason_email": "Email",
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -367,7 +368,9 @@ class DomainRequestAdminForm(forms.ModelForm):
class MultiFieldSortableChangeList(admin.views.main.ChangeList): class MultiFieldSortableChangeList(admin.views.main.ChangeList):
""" """
This class overrides the behavior of column sorting in django admin tables in order This class overrides the behavior of column sorting in django admin tables in order
to allow for multi field sorting on admin_order_field to allow for multi field sorting on admin_order_field. It also overrides behavior
of getting the filter params to allow portfolio filters to be executed without
displaying on the right side of the ChangeList view.
Usage: Usage:
@ -429,6 +432,24 @@ class MultiFieldSortableChangeList(admin.views.main.ChangeList):
return ordering return ordering
def get_filters_params(self, params=None):
"""
Add portfolio to ignored params to allow the portfolio filter while not
listing it as a filter option on the right side of Change List on the
portfolio list.
"""
params = params or self.params
lookup_params = params.copy() # a dictionary of the query string
# Remove all the parameters that are globally and systematically
# ignored.
# Remove portfolio so that it does not error as an invalid
# filter parameter.
ignored_params = list(IGNORED_PARAMS) + ["portfolio"]
for ignored in ignored_params:
if ignored in lookup_params:
del lookup_params[ignored]
return lookup_params
class CustomLogEntryAdmin(LogEntryAdmin): class CustomLogEntryAdmin(LogEntryAdmin):
"""Overwrite the generated LogEntry admin class""" """Overwrite the generated LogEntry admin class"""
@ -645,6 +666,19 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
) )
except models.User.DoesNotExist: except models.User.DoesNotExist:
pass pass
elif parameter_name == "portfolio":
# Retrieves the corresponding portfolio from Portfolio
id_value = request.GET.get(param)
try:
portfolio = models.Portfolio.objects.get(id=id_value)
filters.append(
{
"parameter_name": "portfolio",
"parameter_value": portfolio.organization_name,
}
)
except models.Portfolio.DoesNotExist:
pass
else: else:
# For other parameter names, append a dictionary with the original # For other parameter names, append a dictionary with the original
# parameter_name and the corresponding parameter_value # parameter_name and the corresponding parameter_value
@ -2262,6 +2296,17 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
use_sort = db_field.name != "senior_official" use_sort = db_field.name != "senior_official"
return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs) return super().formfield_for_foreignkey(db_field, request, use_admin_sort_fields=use_sort, **kwargs)
def get_queryset(self, request):
"""Custom get_queryset to filter by portfolio if portfolio is in the
request params."""
qs = super().get_queryset(request)
# Check if a 'portfolio' parameter is passed in the request
portfolio_id = request.GET.get("portfolio")
if portfolio_id:
# Further filter the queryset by the portfolio
qs = qs.filter(portfolio=portfolio_id)
return qs
class TransitionDomainAdmin(ListHeaderAdmin): class TransitionDomainAdmin(ListHeaderAdmin):
"""Custom transition domain admin class.""" """Custom transition domain admin class."""
@ -2715,6 +2760,17 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return True return True
return super().has_change_permission(request, obj) return super().has_change_permission(request, obj)
def get_queryset(self, request):
"""Custom get_queryset to filter by portfolio if portfolio is in the
request params."""
qs = super().get_queryset(request)
# Check if a 'portfolio' parameter is passed in the request
portfolio_id = request.GET.get("portfolio")
if portfolio_id:
# Further filter the queryset by the portfolio
qs = qs.filter(domain_info__portfolio=portfolio_id)
return qs
class DraftDomainResource(resources.ModelResource): class DraftDomainResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -2899,7 +2955,7 @@ class PortfolioAdmin(ListHeaderAdmin):
# "classes": ("collapse", "closed"), # "classes": ("collapse", "closed"),
# "fields": ["administrators", "members"]} # "fields": ["administrators", "members"]}
# ), # ),
("Portfolio domains", {"classes": ("collapse", "closed"), "fields": ["domains", "domain_requests"]}), ("Portfolio domains", {"fields": ["domains", "domain_requests"]}),
("Type of organization", {"fields": ["organization_type", "federal_type"]}), ("Type of organization", {"fields": ["organization_type", "federal_type"]}),
( (
"Organization name and mailing address", "Organization name and mailing address",
@ -2981,18 +3037,27 @@ class PortfolioAdmin(ListHeaderAdmin):
suborganizations.short_description = "Suborganizations" # type: ignore suborganizations.short_description = "Suborganizations" # type: ignore
def domains(self, obj: models.Portfolio): def domains(self, obj: models.Portfolio):
"""Returns a list of links for each related domain""" """Returns the count of domains with a link to view them in the admin."""
queryset = obj.get_domains() domain_count = obj.get_domains().count() # Count the related domains
return self.get_field_links_as_list( if domain_count > 0:
queryset, "domaininformation", link_info_attribute="get_state_display_of_domain" # Construct the URL to the admin page, filtered by portfolio
) url = reverse("admin:registrar_domain_changelist") + f"?portfolio={obj.id}"
label = "domain" if domain_count == 1 else "domains"
# Create a clickable link with the domain count
return format_html('<a href="{}">{} {}</a>', url, domain_count, label)
return "No domains"
domains.short_description = "Domains" # type: ignore domains.short_description = "Domains" # type: ignore
def domain_requests(self, obj: models.Portfolio): def domain_requests(self, obj: models.Portfolio):
"""Returns a list of links for each related domain request""" """Returns the count of domain requests with a link to view them in the admin."""
queryset = obj.get_domain_requests() domain_request_count = obj.get_domain_requests().count() # Count the related domain requests
return self.get_field_links_as_list(queryset, "domainrequest", link_info_attribute="get_status_display") if domain_request_count > 0:
# Construct the URL to the admin page, filtered by portfolio
url = reverse("admin:registrar_domainrequest_changelist") + f"?portfolio={obj.id}"
# Create a clickable link with the domain request count
return format_html('<a href="{}">{} domain requests</a>', url, domain_request_count)
return "No domain requests"
domain_requests.short_description = "Domain requests" # type: ignore domain_requests.short_description = "Domain requests" # type: ignore

View file

@ -353,7 +353,7 @@ function initializeWidgetOnList(list, parentId) {
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason') let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
// This is the "action needed reason" field // This is the "action needed reason" field
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason'); let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
// This is the "auto-generated email" field // This is the "Email" field
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email') let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) { if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
@ -509,22 +509,38 @@ function initializeWidgetOnList(list, parentId) {
(function () { (function () {
// Since this is an iife, these vars will be removed from memory afterwards // Since this is an iife, these vars will be removed from memory afterwards
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason"); 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"); // Placeholder text (for certain "action needed" reasons that do not involve e=mails)
var placeholderText = document.querySelector("#action-needed-reason-email-placeholder-text")
// 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 actionNeededEmailAlreadySentModal = document.querySelector("#email-already-sent-modal")
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 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'); let emailData = document.getElementById('action-needed-emails-data');
if (!emailData) { if (!emailData) {
return; return;
} }
let actionNeededEmailData = emailData.textContent; let actionNeededEmailData = emailData.textContent;
if(!actionNeededEmailData) { if(!actionNeededEmailData) {
return; return;
} }
let actionNeededEmailsJson = JSON.parse(actionNeededEmailData); let actionNeededEmailsJson = JSON.parse(actionNeededEmailData);
const domainRequestId = actionNeededReasonDropdown ? document.querySelector("#domain_request_id").value : null const domainRequestId = actionNeededReasonDropdown ? document.querySelector("#domain_request_id").value : null
const emailSentSessionVariableName = `actionNeededEmailSent-${domainRequestId}`; const emailSentSessionVariableName = `actionNeededEmailSent-${domainRequestId}`;
const oldDropdownValue = actionNeededReasonDropdown ? actionNeededReasonDropdown.value : null; const oldDropdownValue = actionNeededReasonDropdown ? actionNeededReasonDropdown.value : null;
@ -545,52 +561,111 @@ function initializeWidgetOnList(list, parentId) {
updateActionNeededEmailDisplay(reason) 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 // Add a change listener to the action needed reason dropdown
actionNeededReasonDropdown.addEventListener("change", function() { actionNeededReasonDropdown.addEventListener("change", function() {
let reason = actionNeededReasonDropdown.value; let reason = actionNeededReasonDropdown.value;
let emailBody = reason in actionNeededEmailsJson ? actionNeededEmailsJson[reason] : null; let emailBody = reason in actionNeededEmailsJson ? actionNeededEmailsJson[reason] : null;
if (reason && emailBody) {
// Replace the email content
actionNeededEmail.value = emailBody;
if (reason && emailBody) {
// Reset the session object on change since change refreshes the email content. // Reset the session object on change since change refreshes the email content.
if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) { if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) {
let emailSent = sessionStorage.getItem(emailSentSessionVariableName) // Replace the email content
if (emailSent !== null){ actionNeededEmail.value = emailBody;
addOrRemoveSessionBoolean(emailSentSessionVariableName, add=false) actionNeededEmailReadonlyTextarea.value = emailBody;
} hideEmailAlreadySentView();
} }
} }
// Show an editable email field or a readonly one // Show either a preview of the email or some text describing no email will be sent
updateActionNeededEmailDisplay(reason) updateActionNeededEmailDisplay(reason)
}); });
} }
// Shows an editable email field or a readonly one. function checkEmailAlreadySent()
{
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 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. // If the email doesn't exist or if we're of reason "other", display that no email was sent.
// Likewise, if we've sent this email before, we should just display the content.
function updateActionNeededEmailDisplay(reason) { function updateActionNeededEmailDisplay(reason) {
let emailHasBeenSentBefore = sessionStorage.getItem(emailSentSessionVariableName) !== null;
let collapseableDiv = readonlyView.querySelector(".collapse--dgsimple");
let showMoreButton = document.querySelector("#action_needed_reason_email__show_details");
if ((reason && reason != "other") && !emailHasBeenSentBefore) {
showElement(actionNeededEmail.parentElement)
hideElement(readonlyView)
hideElement(showMoreButton)
} else {
if (!reason || reason === "other") {
collapseableDiv.innerHTML = reason ? "No email will be sent." : "-";
hideElement(showMoreButton)
if (collapseableDiv.classList.contains("collapsed")) {
showMoreButton.click()
}
}else {
showElement(showMoreButton)
}
hideElement(actionNeededEmail.parentElement) hideElement(actionNeededEmail.parentElement)
showElement(readonlyView)
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 {
// Hide email preview and show this text instead
showPlaceholderText("Select an action needed reason to see 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)
}
// Hides preview of action needed email and instead displays the given text (innerHTML)
function showPlaceholderText(innerHTML)
{
hideElement(actionNeededEmail.parentElement)
hideElement(actionNeededEmailReadonly)
hideElement(actionNeededEmailFooter)
placeholderText.innerHTML = innerHTML;
showElement(placeholderText)
} }
})(); })();

View file

@ -66,6 +66,9 @@ html[data-theme="light"] {
// --object-tools-fg: var(--button-fg); // --object-tools-fg: var(--button-fg);
// --object-tools-bg: var(--close-button-bg); // --object-tools-bg: var(--close-button-bg);
// --object-tools-hover-bg: var(--close-button-hover-bg); // --object-tools-hover-bg: var(--close-button-hover-bg);
--summary-box-bg: #f1f1f1;
--summary-box-border: #d1d2d2;
} }
// Fold dark theme settings into our main CSS // Fold dark theme settings into our main CSS
@ -104,6 +107,9 @@ html[data-theme="light"] {
--close-button-bg: #333333; --close-button-bg: #333333;
--close-button-hover-bg: #666666; --close-button-hover-bg: #666666;
--summary-box-bg: #121212;
--summary-box-border: #666666;
} }
// Dark mode django (bug due to scss cascade) and USWDS tables // Dark mode django (bug due to scss cascade) and USWDS tables
@ -848,6 +854,26 @@ 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};
}
ul.add-list-reset { ul.add-list-reset {
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;

View file

@ -145,20 +145,110 @@ 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 id="action-needed-reason-email-readonly" class="readonly margin-top-0 padding-top-0 display-none"> <div>
<div class="margin-top-05 collapse--dgsimple collapsed"> <div id="action-needed-reason-email-placeholder-text" class="margin-top-05 text-faded">
{{ field.field.value|linebreaks }} -
</div> </div>
<button id="action_needed_reason_email__show_details" type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1"> <div>
<span>Show details</span> <div id="action-needed-reason-email-readonly" class="display-none usa-summary-box_admin padding-top-0 margin-top-0">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <div class="flex-container">
<use xlink:href="/public/img/sprite.svg#expand_more"></use> <div class="margin-top-05">
<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>
<p class="{% if not action_needed_email_sent %}display-none{% endif %}" id="action-needed-email-header-email-sent">
<svg class="usa-icon text-green" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
</svg>
<b>Email sent to the creator</b>
</p>
</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">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button
type="submit"
class="usa-button"
id="email-already-sent-modal_continue-editing-button"
data-close-modal
>
Yes, continue editing
</button>
</li>
<li class="usa-button-group__item">
<button
type="button"
class="usa-button usa-button--unstyled padding-105 text-center"
name="_cancel_edit_email"
data-close-modal
>
Cancel
</button>
</li>
</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> </svg>
</button> </button>
</div> </div>
</div>
<label class="sr-only" for="action-needed-reason-email-readonly-textarea">Email:</label>
<textarea cols="40" rows="10" class="vLargeTextField" id="action-needed-reason-email-readonly-textarea" readonly>{{ field.field.value|striptags }}
</textarea>
</div>
<div> <div>
{{ field.field }} {{ field.field }}
<input id="action-needed-email-sent" class="display-none" value="{{action_needed_email_sent}}"> <input id="action-needed-email-sent" class="display-none" value="{{action_needed_email_sent}}">
<input id="action-needed-email-last-sent-text" class="display-none" value="{{original_object.action_needed_reason_email}}">
</div>
</div>
<span id="action-needed-email-footer" class="help">
{% if not action_needed_email_sent %}
This email will be sent to the creator of this request after saving
{% else %}
This email has been sent to the creator of this request
{% endif %}
</span>
</div> </div>
{% else %} {% else %}
{{ field.field }} {{ field.field }}

View file

@ -2107,9 +2107,7 @@ class TestPortfolioAdmin(TestCase):
domain_2.save() domain_2.save()
domains = self.admin.domains(self.portfolio) domains = self.admin.domains(self.portfolio)
self.assertIn("domain1.gov", domains) self.assertIn("2 domains", domains)
self.assertIn("domain2.gov", domains)
self.assertIn('<ul class="add-list-reset">', domains)
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_requests_display(self): def test_domain_requests_display(self):
@ -2118,6 +2116,4 @@ class TestPortfolioAdmin(TestCase):
completed_domain_request(name="request2.gov", portfolio=self.portfolio) completed_domain_request(name="request2.gov", portfolio=self.portfolio)
domain_requests = self.admin.domain_requests(self.portfolio) domain_requests = self.admin.domain_requests(self.portfolio)
self.assertIn("request1.gov", domain_requests) self.assertIn("2 domain requests", domain_requests)
self.assertIn("request2.gov", domain_requests)
self.assertIn('<ul class="add-list-reset">', domain_requests)

View file

@ -14,7 +14,9 @@ from registrar.models import (
DomainInformation, DomainInformation,
User, User,
Host, Host,
Portfolio,
) )
from registrar.models.user_domain_role import UserDomainRole
from .common import ( from .common import (
MockSESClient, MockSESClient,
completed_domain_request, completed_domain_request,
@ -356,9 +358,11 @@ class TestDomainAdminWithClient(TestCase):
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
Host.objects.all().delete() Host.objects.all().delete()
UserDomainRole.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete() DomainRequest.objects.all().delete()
Portfolio.objects.all().delete()
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(self):
@ -452,6 +456,36 @@ class TestDomainAdminWithClient(TestCase):
domain_request.delete() domain_request.delete()
_creator.delete() _creator.delete()
@less_console_noise_decorator
def test_domains_by_portfolio(self):
"""
Tests that domains display for a portfolio. And that domains outside the portfolio do not display.
"""
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test Portfolio", creator=self.superuser)
# Create a fake domain request and domain
_domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, portfolio=portfolio
)
_domain_request.approve()
domain = _domain_request.approved_domain
domain2, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
UserDomainRole.objects.get_or_create()
UserDomainRole.objects.get_or_create(user=self.superuser, domain=domain2, role=UserDomainRole.Roles.MANAGER)
self.client.force_login(self.superuser)
response = self.client.get(
"/admin/registrar/domain/?portfolio={}".format(portfolio.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertNotContains(response, domain2.name)
self.assertContains(response, portfolio.organization_name)
@less_console_noise_decorator @less_console_noise_decorator
def test_helper_text(self): def test_helper_text(self):
""" """

View file

@ -22,6 +22,7 @@ from registrar.models import (
Contact, Contact,
Website, Website,
SeniorOfficial, SeniorOfficial,
Portfolio,
AllowedEmail, AllowedEmail,
) )
from .common import ( from .common import (
@ -81,6 +82,7 @@ class TestDomainRequestAdmin(MockEppLib):
Contact.objects.all().delete() Contact.objects.all().delete()
Website.objects.all().delete() Website.objects.all().delete()
SeniorOfficial.objects.all().delete() SeniorOfficial.objects.all().delete()
Portfolio.objects.all().delete()
self.mock_client.EMAILS_SENT.clear() self.mock_client.EMAILS_SENT.clear()
@classmethod @classmethod
@ -267,6 +269,33 @@ class TestDomainRequestAdmin(MockEppLib):
self.assertContains(response, domain_request.requested_domain.name) self.assertContains(response, domain_request.requested_domain.name)
self.assertContains(response, "<span>Show details</span>") self.assertContains(response, "<span>Show details</span>")
@less_console_noise_decorator
def test_domain_requests_by_portfolio(self):
"""
Tests that domain_requests display for a portfolio. And requests not in portfolio do not display.
"""
portfolio, _ = Portfolio.objects.get_or_create(organization_name="Test Portfolio", creator=self.superuser)
# Create a fake domain request and domain
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.IN_REVIEW, portfolio=portfolio
)
domain_request2 = completed_domain_request(
name="testdomain2.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
)
self.client.force_login(self.superuser)
response = self.client.get(
"/admin/registrar/domainrequest/?portfolio={}".format(portfolio.pk),
follow=True,
)
# Make sure the page loaded, and that we're on the right page
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain_request.requested_domain.name)
self.assertNotContains(response, domain_request2.requested_domain.name)
self.assertContains(response, portfolio.organization_name)
@less_console_noise_decorator @less_console_noise_decorator
def test_analyst_can_see_and_edit_alternative_domain(self): def test_analyst_can_see_and_edit_alternative_domain(self):
"""Tests if an analyst can still see and edit the alternative domain field""" """Tests if an analyst can still see and edit the alternative domain field"""

View file

@ -4,6 +4,8 @@ from registrar.models import FederalAgency, SeniorOfficial, User
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
from api.tests.common import less_console_noise_decorator
class GetSeniorOfficialJsonTest(TestCase): class GetSeniorOfficialJsonTest(TestCase):
def setUp(self): def setUp(self):
@ -26,6 +28,7 @@ class GetSeniorOfficialJsonTest(TestCase):
SeniorOfficial.objects.all().delete() SeniorOfficial.objects.all().delete()
FederalAgency.objects.all().delete() FederalAgency.objects.all().delete()
@less_console_noise_decorator
def test_get_senior_official_json_authenticated_superuser(self): def test_get_senior_official_json_authenticated_superuser(self):
"""Test that a superuser can fetch the senior official information.""" """Test that a superuser can fetch the senior official information."""
p = "adminpass" p = "adminpass"
@ -38,6 +41,7 @@ class GetSeniorOfficialJsonTest(TestCase):
self.assertEqual(data["last_name"], "Doe") self.assertEqual(data["last_name"], "Doe")
self.assertEqual(data["title"], "Director") self.assertEqual(data["title"], "Director")
@less_console_noise_decorator
def test_get_senior_official_json_authenticated_analyst(self): def test_get_senior_official_json_authenticated_analyst(self):
"""Test that an analyst user can fetch the senior official's information.""" """Test that an analyst user can fetch the senior official's information."""
p = "userpass" p = "userpass"
@ -50,6 +54,7 @@ class GetSeniorOfficialJsonTest(TestCase):
self.assertEqual(data["last_name"], "Doe") self.assertEqual(data["last_name"], "Doe")
self.assertEqual(data["title"], "Director") self.assertEqual(data["title"], "Director")
@less_console_noise_decorator
def test_get_senior_official_json_unauthenticated(self): def test_get_senior_official_json_unauthenticated(self):
"""Test that an unauthenticated user receives a 403 with an error message.""" """Test that an unauthenticated user receives a 403 with an error message."""
p = "password" p = "password"
@ -57,6 +62,7 @@ class GetSeniorOfficialJsonTest(TestCase):
response = self.client.get(self.api_url, {"agency_name": "Test Agency"}) response = self.client.get(self.api_url, {"agency_name": "Test Agency"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
@less_console_noise_decorator
def test_get_senior_official_json_not_found(self): def test_get_senior_official_json_not_found(self):
"""Test that a request for a non-existent agency returns a 404 with an error message.""" """Test that a request for a non-existent agency returns a 404 with an error message."""
p = "adminpass" p = "adminpass"