Merge branch 'main' into hotgov/2355-rejection-reason-emails

This commit is contained in:
zandercymatics 2024-10-09 08:33:26 -06:00
commit fc8090dc15
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
13 changed files with 1186 additions and 127 deletions

View file

@ -1973,18 +1973,30 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# If the status is not mapped properly, saving could cause
# weird issues down the line. Instead, we should block this.
# NEEDS A UNIT TEST
should_proceed = False
return should_proceed
return (obj, should_proceed)
request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
if request_is_not_approved and not obj.domain_is_not_active():
# If an admin tried to set an approved domain request to
# another status and the related domain is already
# active, shortcut the action and throw a friendly
# error message. This action would still not go through
# shortcut or not as the rules are duplicated on the model,
# but the error would be an ugly Django error screen.
obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
if obj_is_not_approved and not obj.domain_is_not_active():
# REDUNDANT CHECK / ERROR SCREEN AVOIDANCE:
# This action (moving a request from approved to
# another status) when the domain is already active (READY),
# would still not go through even without this check as the rules are
# duplicated in the model and the error is raised from the model.
# This avoids an ugly Django error screen.
error_message = "This action is not permitted. The domain is already active."
elif (
original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED
and original_obj.requested_domain is not None
and Domain.objects.filter(name=original_obj.requested_domain.name).exists()
):
# REDUNDANT CHECK:
# This action (approving a request when the domain exists)
# would still not go through even without this check as the rules are
# duplicated in the model and the error is raised from the model.
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.APPROVE_DOMAIN_IN_USE)
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
# This condition should never be triggered.
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)

View file

@ -848,18 +848,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
return '';
}
// Extract the submitter name, title, email, and phone number
const submitterDiv = document.querySelector('.form-row.field-submitter');
const submitterNameElement = document.getElementById('id_submitter');
// We have to account for different superuser and analyst markups
const submitterName = submitterNameElement
? submitterNameElement.options[submitterNameElement.selectedIndex].text
: submitterDiv.querySelector('a').text;
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`;
//------ Senior Official
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
@ -876,7 +864,6 @@ document.addEventListener('DOMContentLoaded', function() {
`<strong>Current Websites:</strong> ${existingWebsites.join(', ')}</br>` +
`<strong>Rationale:</strong></br>` +
`<strong>Alternative Domains:</strong> ${alternativeDomains.join(', ')}</br>` +
`<strong>Submitter:</strong> ${submitterInfo}</br>` +
`<strong>Senior Official:</strong> ${seniorOfficialInfo}</br>` +
`<strong>Other Employees:</strong> ${otherContactsSummary}</br>`;

View file

@ -385,6 +385,7 @@ a.button,
font-kerning: auto;
font-family: inherit;
font-weight: normal;
text-decoration: none !important;
}
.button svg,
.button span,
@ -392,6 +393,9 @@ a.button,
.usa-button--dja span {
vertical-align: middle;
}
.usa-button--dja.usa-button--unstyled {
color: var(--link-fg);
}
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
background: var(--button-bg);
}
@ -421,11 +425,34 @@ input[type=submit].button--dja-toolbar {
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
border-color: var(--body-quiet-color);
}
// Targets the DJA buttom with a nested icon
button .usa-icon,
.button .usa-icon,
.button--clipboard .usa-icon {
vertical-align: middle;
.admin-icon-group {
position: relative;
display: inline;
align-items: center;
input {
// Allow for padding around the copy button
padding-right: 35px !important;
}
button {
width: max-content;
}
@media (max-width: 1000px) {
button {
display: block;
}
}
span {
padding-left: 0.05rem;
}
}
.usa-button__small-text,
.usa-button__small-text span {
font-size: 13px;
}
.module--custom {
@ -673,71 +700,10 @@ address.dja-address-contact-list {
}
}
// Make the clipboard button "float" inside of the input box
.admin-icon-group {
position: relative;
display: inline;
align-items: center;
input {
// Allow for padding around the copy button
padding-right: 35px !important;
// Match the height of other inputs
min-height: 2.25rem !important;
}
button {
line-height: 14px;
width: max-content;
font-size: unset;
text-decoration: none !important;
}
@media (max-width: 1000px) {
button {
display: block;
padding-top: 8px;
}
}
span {
padding-left: 0.1rem;
}
}
.admin-icon-group.admin-icon-group__clipboard-link {
position: relative;
display: inline;
align-items: center;
.usa-button--icon {
position: absolute;
right: auto;
left: 4px;
height: 100%;
top: -1px;
}
button {
font-size: unset !important;
display: inline-flex;
padding-top: 4px;
line-height: 14px;
width: max-content;
font-size: unset;
text-decoration: none !important;
}
}
.no-outline-on-click:focus {
outline: none !important;
}
.usa-button__small-text {
font-size: small;
}
// Get rid of padding on all help texts
form .aligned p.help, form .aligned div.help {
padding-left: 0px !important;
@ -887,6 +853,9 @@ div.dja__model-description{
padding-top: 0 !important;
}
.padding-bottom-0 {
padding-bottom: 0 !important;
}
.flex-container {
@media screen and (min-width: 700px) and (max-width: 1150px) {

View file

@ -20,10 +20,11 @@
</li>
{% if opts.model_name == 'domainrequest' %}
<li>
<a id="id-copy-to-clipboard-summary" class="button--clipboard" type="button" href="#">
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
<svg class="usa-icon" >
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
<span>{% translate "Copy request summary" %}</span>
</a>
</li>

View file

@ -8,7 +8,7 @@ Template for an input field with a clipboard
<div class="admin-icon-group">
{{ field }}
<button
class="usa-button usa-button--unstyled padding-left-1 usa-button--icon button--clipboard copy-to-clipboard"
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-left-1 usa-button--icon copy-to-clipboard"
type="button"
>
<div class="no-outline-on-click">
@ -17,23 +17,27 @@ Template for an input field with a clipboard
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
Copy
<!-- the span is targeted in JS, do not remove -->
<span>Copy</span>
</div>
</button>
</div>
{% else %}
<div class="admin-icon-group admin-icon-group__clipboard-link">
<div class="admin-icon-group">
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
<button
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard text-no-underline padding-left-05"
type="button"
>
<svg
class="usa-icon"
{% if field.email is not None %}
<button
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-right-1 usa-button--icon copy-to-clipboard text-no-underline padding-left-05"
type="button"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
Copy
</button>
<svg
class="usa-icon"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
<span>Copy</span>
</button>
{% endif %}
</div>
{% endif %}
{% endif %}

View file

@ -26,7 +26,7 @@
{% if user.email %}
<span id="contact_info_email">{{ user.email }}</span>
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
<br class="admin-icon-group__br">
<br>
{% else %}
None<br>
{% endif %}

View file

@ -342,7 +342,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
<table>
<thead>
<tr>
<th colspan="4">Other contact information</th>
<th colspan="5">Other contact information</th>
<tr>
</thead>
<tbody>
@ -355,18 +355,31 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
</td>
<td class="padding-left-1">{{ contact.phone }}</td>
<td class="padding-left-1 text-size-small">
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
<button
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard usa-button__small-text text-no-underline"
type="button"
>
<svg
class="usa-icon"
{% if contact.email %}
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
<button
class="
usa-button--dja
usa-button
usa-button__small-text
usa-button--unstyled
padding-right-1
padding-top-0
padding-bottom-0
usa-button--icon
copy-to-clipboard
text-no-underline"
type="button"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<span>Copy email</span>
</button>
<svg
class="usa-icon"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
</svg>
<!-- the span is targeted in JS, do not remove -->
<span>Copy email</span>
</button>
{% endif %}
</td>
</tr>
{% endfor %}

View file

@ -654,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
self.assertContains(response, "button--clipboard", count=3)
self.assertContains(response, "copy-to-clipboard", count=3)
# cleanup this test
domain_info.delete()

View file

@ -535,7 +535,7 @@ class TestDomainAdminWithClient(TestCase):
self.assertContains(response, "Testy Tester")
# Test for the copy link
self.assertContains(response, "button--clipboard")
self.assertContains(response, "copy-to-clipboard")
# cleanup from this test
domain.delete()

View file

@ -1527,7 +1527,7 @@ class TestDomainRequestAdmin(MockEppLib):
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
# Test for the copy link
self.assertContains(response, "button--clipboard", count=4)
self.assertContains(response, "copy-to-clipboard", count=4)
# Test that Creator counts display properly
self.assertNotContains(response, "Approved domains")
@ -1863,6 +1863,58 @@ class TestDomainRequestAdmin(MockEppLib):
def test_side_effects_when_saving_approved_to_ineligible(self):
self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE)
@less_console_noise
def test_error_when_saving_to_approved_and_domain_exists(self):
"""Redundant admin check on model transition not allowed."""
Domain.objects.create(name="wabbitseason.gov")
new_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.SUBMITTED, name="wabbitseason.gov"
)
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
request.user = self.superuser
request.session = {}
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch django.contrib.messages.error
stack.enter_context(patch.object(messages, "error"))
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
self.admin.save_model(request, new_request, None, True)
messages.error.assert_called_once_with(
request,
"Cannot approve. Requested domain is already in use.",
)
@less_console_noise
def test_no_error_when_saving_to_approved_and_domain_exists(self):
"""The negative of the redundant admin check on model transition not allowed."""
new_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED)
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
request.user = self.superuser
request.session = {}
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(messages, "error"))
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
self.admin.save_model(request, new_request, None, True)
# Assert that the error message was never called
messages.error.assert_not_called()
def test_has_correct_filters(self):
"""
This test verifies that DomainRequestAdmin has the correct filters set up.

View file

@ -1,7 +1,5 @@
from django.forms import ValidationError
from django.test import TestCase
from django.db.utils import IntegrityError
from django.db import transaction
from unittest.mock import patch
from django.test import RequestFactory
@ -20,23 +18,18 @@ from registrar.models import (
UserPortfolioPermission,
AllowedEmail,
)
import boto3_mocking
from registrar.models.portfolio import Portfolio
from registrar.models.portfolio_invitation import PortfolioInvitation
from registrar.models.transition_domain import TransitionDomain
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore
from registrar.utility.constants import BranchChoices
from .common import (
MockSESClient,
less_console_noise,
completed_domain_request,
set_domain_request_investigators,
create_test_user,
)
from django_fsm import TransitionNotAllowed
from waffle.testutils import override_flag
import logging

File diff suppressed because it is too large Load diff

View file

@ -82,7 +82,6 @@ class DomainRequestTests(TestWithUser, WebTest):
response = self.app.get(f"/domain-request/{domain_request.id}")
# Ensure that the date is still set to None
self.assertIsNone(domain_request.last_status_update)
print(response)
# We should still grab a date for this field in this event - but it should come from the audit log instead
self.assertContains(response, "Started on:")
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))