mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-03 09:43:33 +02:00
Merge branch 'main' of github.com:cisagov/manage.get.gov into rh/1500-domain-req-alt-req-same
This commit is contained in:
commit
92e7e40ee2
50 changed files with 729 additions and 128 deletions
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Which OIDC provider to use
|
||||
OIDC_ACTIVE_PROVIDER: login.gov production
|
||||
# Flag to disable/enable features in prod environments
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# use a non-default route to avoid conflicts
|
||||
routes:
|
||||
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov
|
||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
|||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
|
|
|
@ -32,7 +32,7 @@ services:
|
|||
# Is this a production environment
|
||||
- IS_PRODUCTION
|
||||
# Public site URL link
|
||||
- GETGOV_PUBLIC_SITE_URL=https://beta.get.gov
|
||||
- GETGOV_PUBLIC_SITE_URL=https://get.gov
|
||||
# Set a username for accessing the registry
|
||||
- REGISTRY_CL_ID=nothing
|
||||
# Set a password for accessing the registry
|
||||
|
|
|
@ -610,7 +610,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating .gov domains",
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating .gov domains",
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -309,9 +309,21 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
|||
// h2 and legend for DS form, label for nameservers
|
||||
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
||||
|
||||
let innerSpan = node.querySelector('span')
|
||||
if (innerSpan) {
|
||||
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
} else {
|
||||
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||
}
|
||||
|
||||
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
||||
// inject the USWDS required markup and make sure the INPUT is required
|
||||
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
||||
|
||||
// Remove the word optional
|
||||
innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, '');
|
||||
|
||||
// Create a new element
|
||||
const newElement = document.createElement('abbr');
|
||||
newElement.textContent = '*';
|
||||
|
@ -334,13 +346,8 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
|||
nextInputElement.required = true;
|
||||
}
|
||||
|
||||
let innerSpan = node.querySelector('span')
|
||||
if (innerSpan) {
|
||||
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
} else {
|
||||
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Display the add more button if we have less than 13 forms
|
||||
|
@ -563,16 +570,24 @@ function hideDeletedForms() {
|
|||
formNum++;
|
||||
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
||||
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
||||
// since the form on the backend employs Django's DELETE widget. For the other formsets, we delete the form
|
||||
// in JS (completely remove from teh DOM) so we update the headers/labels based on total number of forms.
|
||||
if (isOtherContactsForm) {
|
||||
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
||||
// since the form on the backend employs Django's DELETE widget.
|
||||
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
|
||||
} else {
|
||||
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
||||
// if indices 0 or 1 have been deleted
|
||||
let containsOptional = newForm.innerHTML.includes('(optional)');
|
||||
if (isNameserversForm && !containsOptional) {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`);
|
||||
} else {
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||
}
|
||||
}
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/>\s*</g, '><'); // Remove spaces between tags
|
||||
container.insertBefore(newForm, addButton);
|
||||
|
||||
newForm.style.display = 'block';
|
||||
|
|
|
@ -25,6 +25,22 @@
|
|||
color: color('primary-darker');
|
||||
padding-bottom: units(2px);
|
||||
}
|
||||
|
||||
// Ticket #1510
|
||||
// @include at-media('desktop') {
|
||||
// th:first-child {
|
||||
// width: 220px;
|
||||
// }
|
||||
// th:nth-child(2) {
|
||||
// width: 175px;
|
||||
// }
|
||||
// th:nth-child(3) {
|
||||
// width: 130px;
|
||||
// }
|
||||
// th:nth-child(5) {
|
||||
// width: 130px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.dotgov-table {
|
||||
|
|
|
@ -335,7 +335,7 @@ CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
|||
# Cross-Origin Resource Sharing (CORS) configuration
|
||||
# Sets clients that allow access control to manage.get.gov
|
||||
# TODO: remove :8080 to see if we can have all localhost access
|
||||
CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov"]
|
||||
CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov", "https://get.gov"]
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = [r"https://[\w-]+\.sites\.pages\.cloud\.gov"]
|
||||
|
||||
# Content-Length header is set by django.middleware.common.CommonMiddleware
|
||||
|
|
|
@ -137,6 +137,11 @@ urlpatterns = [
|
|||
views.DomainInvitationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="invitation-delete",
|
||||
),
|
||||
path(
|
||||
"application/<int:pk>/delete",
|
||||
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="application-delete",
|
||||
),
|
||||
]
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
|
|
|
@ -487,7 +487,8 @@ class DotGovDomainForm(RegistrarForm):
|
|||
values = {}
|
||||
requested_domain = getattr(obj, "requested_domain", None)
|
||||
if requested_domain is not None:
|
||||
values["requested_domain"] = Domain.sld(requested_domain.name)
|
||||
domain_name = requested_domain.name
|
||||
values["requested_domain"] = Domain.sld(domain_name)
|
||||
return values
|
||||
|
||||
def clean_requested_domain(self):
|
||||
|
@ -837,8 +838,8 @@ class AnythingElseForm(RegistrarForm):
|
|||
|
||||
class RequirementsForm(RegistrarForm):
|
||||
is_policy_acknowledged = forms.BooleanField(
|
||||
label="I read and agree to the requirements for operating .gov domains.",
|
||||
label="I read and agree to the requirements for operating a .gov domain.",
|
||||
error_messages={
|
||||
"required": ("Check the box if you read and agree to the requirements for operating .gov domains.")
|
||||
"required": ("Check the box if you read and agree to the requirements for operating a .gov domain.")
|
||||
},
|
||||
)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
""""
|
||||
Converts all ready and DNS needed domains with a non-default public contact
|
||||
to disclose their public contact. Created for Issue#1535 to resolve
|
||||
disclose issue of domains with missing security emails.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from registrar.models import Domain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Disclose all nondefault domain security emails."
|
||||
|
||||
def __init__(self):
|
||||
"""Sets global variables for code tidiness"""
|
||||
super().__init__()
|
||||
# domains with errors, which are not successfully updated to disclose
|
||||
self.domains_with_errors: list[str] = []
|
||||
# domains that are successfully disclosed
|
||||
self.disclosed_domain_contacts_count = 0
|
||||
# domains that skip disclose due to having contact registrar@dotgov.gov
|
||||
self.skipped_domain_contacts_count = 0
|
||||
|
||||
def handle(self, **options):
|
||||
"""
|
||||
Converts all ready and DNS needed domains with a non-default public contact
|
||||
to disclose their public contact.
|
||||
"""
|
||||
logger.info("Updating security emails to public")
|
||||
|
||||
# Initializes domains that need to be disclosed
|
||||
|
||||
statuses = [Domain.State.READY, Domain.State.DNS_NEEDED]
|
||||
domains = Domain.objects.filter(state__in=statuses)
|
||||
|
||||
logger.info(f"Found {len(domains)} domains with status Ready or DNS Needed.")
|
||||
|
||||
# Update EPP contact for domains with a security contact
|
||||
for domain in domains:
|
||||
try:
|
||||
contact = domain.security_contact # noqa on these items as we only want to call security_contact
|
||||
logger.info(f"Domain {domain.name} security contact: {domain.security_contact.email}")
|
||||
if domain.security_contact.email != "registrar@dotgov.gov":
|
||||
domain._update_epp_contact(contact=domain.security_contact)
|
||||
self.disclosed_domain_contacts_count += 1
|
||||
else:
|
||||
logger.info(
|
||||
f"Skipping disclose for {domain.name} security contact {domain.security_contact.email}."
|
||||
)
|
||||
self.skipped_domain_contacts_count += 1
|
||||
except Exception as err:
|
||||
# error condition if domain not in database
|
||||
self.domains_with_errors.append(copy.deepcopy(domain.name))
|
||||
logger.error(f"error retrieving domain {domain.name} contact {domain.security_contact}: {err}")
|
||||
|
||||
# Inform user how many contacts were disclosed, skipped, and errored
|
||||
logger.info(f"Updated {self.disclosed_domain_contacts_count} contacts to disclosed.")
|
||||
logger.info(
|
||||
f"Skipped disclosing {self.skipped_domain_contacts_count} contacts with security email "
|
||||
f"registrar@dotgov.gov."
|
||||
)
|
||||
logger.info(
|
||||
f"Error disclosing the following {len(self.domains_with_errors)} contacts: {self.domains_with_errors}"
|
||||
)
|
|
@ -1396,11 +1396,13 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
def _disclose_fields(self, contact: PublicContact):
|
||||
"""creates a disclose object that can be added to a contact Create using
|
||||
.disclose= <this function> on the command before sending.
|
||||
if item is security email then make sure email is visable"""
|
||||
if item is security email then make sure email is visible"""
|
||||
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
|
||||
DF = epp.DiscloseField
|
||||
fields = {DF.EMAIL}
|
||||
disclose = is_security and contact.email != PublicContact.get_default_security().email
|
||||
# Delete after testing on other devices
|
||||
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
|
||||
# Will only disclose DF.EMAIL if its not the default
|
||||
return epp.Disclose(
|
||||
flag=disclose,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static form_helpers url_helpers %}
|
||||
|
||||
{% block title %}Apply for a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
||||
{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row grid-gap">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% load field_helpers %}
|
||||
{% load field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="https://beta.get.gov/about/data/" target="_blank">.gov’s public data.</a></p>
|
||||
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="{% public_site_url 'about/data/' %}" target="_blank">.gov’s public data.</a></p>
|
||||
|
||||
<h2>What is the name and mailing address of the organization you represent?</h2>
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization.
|
||||
<p>To help us determine your organization’s eligibility for a .gov domain, it’s helpful to have contact information for other employees from your organization.
|
||||
<ul class="usa-list">
|
||||
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request.</li>
|
||||
<li>They don't need to be involved with the technical management of your domain (although they can be). </li>
|
||||
<li>We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate first with you, the requestor. </li>
|
||||
<li>They don’t need to be involved with the technical management of your domain (although they can be).</li>
|
||||
<li><strong>We typically don’t reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
|
@ -88,9 +88,7 @@
|
|||
<legend>
|
||||
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
|
||||
</legend>
|
||||
<p>You don't need to provide names of other employees now, but it may
|
||||
slow down our assessment of your eligibility. Describe why there are
|
||||
no other employees who can help verify your request.</p>
|
||||
<p>You don’t need to provide names of other employees now, but it may slow down our assessment of your eligibility. Describe why there are no other employees who can help verify your request.</p>
|
||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.no_other_contacts_rationale %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% load field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p>
|
||||
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
|
||||
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
||||
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% if domainapplication.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current website for your organization' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.requested_domain %}
|
||||
|
|
|
@ -70,19 +70,6 @@
|
|||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
||||
|
||||
{% if IS_DEMO_SITE %}
|
||||
<section aria-label="Alert" >
|
||||
<div class="usa-alert usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">New domain requests are paused</h4>
|
||||
<p class="usa-alert__text measure-none">
|
||||
This is the new registrar for managing .gov domains. Note that we’re not accepting requests for new .gov domains until January 2024. Follow .gov updates at <a href="https://get.gov/updates/" class="usa-link">get.gov/updates/</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="usa-banner" aria-label="Official website of the United States government">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
{% if widget.attrs.required %}
|
||||
<!--Don't add asterisk to one-field forms -->
|
||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
||||
{% else %}
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block title %}Add another user | {% endblock %}
|
||||
{% block title %}Add a domain manager | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
<h1>Add a domain manager</h1>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov"%}Add security email{% else %}Save{% endif %}</button>
|
||||
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov"%}Add security email{% else %}Save{% endif %}</button>
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -17,7 +17,7 @@ About your organization:
|
|||
Authorizing official:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
||||
{% if application.current_websites.exists %}{# if block makes a newline #}
|
||||
Current website for your organization: {% for site in application.current_websites.all %}
|
||||
Current websites: {% for site in application.current_websites.all %}
|
||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
|
|
|
@ -32,6 +32,14 @@ Learn more about:
|
|||
- Domain security best practices <https://get.gov/domains/security/>
|
||||
|
||||
|
||||
WE’LL PRELOAD THIS DOMAIN
|
||||
We add new .gov domains to the HSTS preload list each month. This requires browsers to use a secure HTTPS connection to any website at this domain and ensures the content you publish is exactly what your visitors get. It also means you’ll need to support HTTPS anywhere the domain is used for websites – on the internet or internally. We’ll add your domain to the preload list soon.
|
||||
|
||||
Learn more about:
|
||||
- What preloading is <https://get.gov/domains/security/#preload-your-domain>
|
||||
- Why we preload new domains <https://get.gov/posts/2021-06-21-an-intent-to-preload/>
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||
|
||||
|
|
|
@ -9,29 +9,17 @@
|
|||
{% if user.is_authenticated %}
|
||||
{# the entire logged in page goes here #}
|
||||
|
||||
<div class="tablet:grid-offset-1 desktop:grid-offset-2">
|
||||
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||
<h1>Manage your domains</h2>
|
||||
|
||||
<p class="margin-top-4">
|
||||
{% if IS_PRODUCTION %}
|
||||
<a href="javascript:void(0)"
|
||||
class="usa-button usa-tooltip disabled-link"
|
||||
data-position="right"
|
||||
title="Coming in 2024"
|
||||
aria-disabled="true"
|
||||
data-tooltip="true"
|
||||
>
|
||||
Start a new domain request
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'application:' %}" class="usa-button"
|
||||
>
|
||||
Start a new domain request
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||
<section class="section--outlined">
|
||||
<h2>Domains</h2>
|
||||
{% if domains %}
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
|
@ -41,7 +29,12 @@
|
|||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
||||
<th data-sortable scope="col" role="columnheader">Expires</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
<th
|
||||
scope="col"
|
||||
role="columnheader"
|
||||
>
|
||||
<span class="usa-sr-only">Action</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -58,7 +51,7 @@
|
|||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
||||
DNS needed
|
||||
{% else %}
|
||||
{{ domain.state|title }}
|
||||
{{ domain.state|capfirst }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
@ -94,7 +87,7 @@
|
|||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||
<section class="section--outlined">
|
||||
<h2>Domain requests</h2>
|
||||
{% if domain_applications %}
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
|
@ -105,13 +98,23 @@
|
|||
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
{% if has_deletable_applications %}
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in domain_applications %}
|
||||
<tr>
|
||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||
{{ application.requested_domain.name|default:"New domain request" }}
|
||||
{% if application.requested_domain is None %}
|
||||
New domain request
|
||||
{# Add a breakpoint #}
|
||||
<div aria-hidden="true"></div>
|
||||
<span class="text-base font-body-xs">({{ application.created_at }} UTC)</span>
|
||||
{% else %}
|
||||
{{ application.requested_domain.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
|
||||
{% if application.submission_date %}
|
||||
|
@ -122,22 +125,88 @@
|
|||
</td>
|
||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.status == application.ApplicationStatus.STARTED or application.status == application.ApplicationStatus.ACTION_NEEDED or application.status == application.ApplicationStatus.WITHDRAWN %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
</svg>
|
||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request" }} </span>
|
||||
|
||||
{% if application.requested_domain is not None%}
|
||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Edit <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'application-status' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||
</svg>
|
||||
Manage <span class="usa-sr-only">{{application.requested_domain.name}} </span>
|
||||
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:name_default }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
</td>
|
||||
{% if has_deletable_applications %}
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "withdrawn" %}
|
||||
<a
|
||||
role="button"
|
||||
id="button-toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
href="#toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
class="usa-button--unstyled text-no-underline"
|
||||
aria-controls="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
data-open-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>
|
||||
{% with prefix="New domain request ("%}
|
||||
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||
{% if application.requested_domain is not None %}
|
||||
Delete <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||
{% else %}
|
||||
Delete <span class="usa-sr-only">{{ name_default }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Domain will be removed"
|
||||
data-force-action
|
||||
>
|
||||
<form method="POST" action="{% url "application-delete" pk=application.id %}">
|
||||
{% if application.requested_domain is None %}
|
||||
{% if application.created_at %}
|
||||
{% with prefix="(created " %}
|
||||
{% with formatted_date=application.created_at|date:"DATETIME_FORMAT" %}
|
||||
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% with modal_heading_value=application.requested_domain.name|add:"?" %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
{{ modal_heading }}
|
||||
{% if heading_value is not None %}
|
||||
{# Add a breakpoint #}
|
||||
<div aria-hidden="true"></div>
|
||||
{{ heading_value }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p id="modal-1-description">
|
||||
|
|
|
@ -47,7 +47,7 @@ class TestEmails(TestCase):
|
|||
# check for optional things
|
||||
self.assertIn("Other employees from your organization:", body)
|
||||
self.assertIn("Testy2 Tester2", body)
|
||||
self.assertIn("Current website for your organization:", body)
|
||||
self.assertIn("Current websites:", body)
|
||||
self.assertIn("city.com", body)
|
||||
self.assertIn("About your organization:", body)
|
||||
self.assertIn("Anything else", body)
|
||||
|
@ -61,7 +61,7 @@ class TestEmails(TestCase):
|
|||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Current website for your organization:", body)
|
||||
self.assertNotIn("Current websites:", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5555\n\n.gov domain:")
|
||||
|
||||
|
@ -74,9 +74,9 @@ class TestEmails(TestCase):
|
|||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("Current website for your organization:", body)
|
||||
self.assertIn("Current websites:", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5555\n\nCurrent website for")
|
||||
self.assertRegex(body, r"5555\n\nCurrent websites:")
|
||||
self.assertRegex(body, r"city.com\n\n.gov domain:")
|
||||
|
||||
@boto3_mocking.patching
|
||||
|
|
|
@ -101,7 +101,7 @@ class TestFormValidation(MockEppLib):
|
|||
(
|
||||
"whitehouse.gov",
|
||||
"That domain isn’t available. <a class='usa-link' "
|
||||
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
|
||||
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
|
||||
"choosing your .gov domain</a>.",
|
||||
),
|
||||
]
|
||||
|
@ -151,7 +151,7 @@ class TestFormValidation(MockEppLib):
|
|||
(
|
||||
"whitehouse.gov",
|
||||
"That domain isn’t available. <a class='usa-link' "
|
||||
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
|
||||
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
|
||||
"choosing your .gov domain</a>.",
|
||||
),
|
||||
]
|
||||
|
@ -338,7 +338,7 @@ class TestFormValidation(MockEppLib):
|
|||
form = RequirementsForm(data={})
|
||||
self.assertEqual(
|
||||
form.errors["is_policy_acknowledged"],
|
||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
||||
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||
)
|
||||
|
||||
def test_requirements_form_unchecked(self):
|
||||
|
@ -346,7 +346,7 @@ class TestFormValidation(MockEppLib):
|
|||
form = RequirementsForm(data={"is_policy_acknowledged": False})
|
||||
self.assertEqual(
|
||||
form.errors["is_policy_acknowledged"],
|
||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
||||
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||
)
|
||||
|
||||
def test_tribal_government_unrecognized(self):
|
||||
|
|
|
@ -11,9 +11,11 @@ from registrar.models import (
|
|||
DomainInformation,
|
||||
UserDomainRole,
|
||||
)
|
||||
from registrar.models.public_contact import PublicContact
|
||||
|
||||
from django.core.management import call_command
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, call
|
||||
from epplibwrapper import commands, common
|
||||
|
||||
from .common import MockEppLib
|
||||
|
||||
|
@ -441,3 +443,57 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
|
||||
# Explicitly test the expiration date - should be the same
|
||||
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
|
||||
|
||||
|
||||
class TestDiscloseEmails(MockEppLib):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
|
||||
def run_disclose_security_emails(self):
|
||||
"""
|
||||
This method executes the disclose_security_emails command.
|
||||
|
||||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the disclose_security_emails command.
|
||||
"""
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("disclose_security_emails")
|
||||
|
||||
def test_disclose_security_emails(self):
|
||||
"""
|
||||
Tests that command disclose_security_emails runs successfully with
|
||||
appropriate EPP calll to UpdateContact.
|
||||
"""
|
||||
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
|
||||
expectedSecContact = PublicContact.get_default_security()
|
||||
expectedSecContact.domain = domain
|
||||
expectedSecContact.email = "123@mail.gov"
|
||||
# set domain security email to 123@mail.gov instead of default email
|
||||
domain.security_contact = expectedSecContact
|
||||
self.run_disclose_security_emails()
|
||||
|
||||
# running disclose_security_emails sends EPP call UpdateContact with disclose
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateContact(
|
||||
id=domain.security_contact.registry_id,
|
||||
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
|
||||
email=domain.security_contact.email,
|
||||
voice=domain.security_contact.voice,
|
||||
fax=domain.security_contact.fax,
|
||||
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
disclose=domain._disclose_fields(contact=domain.security_contact),
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -399,7 +399,7 @@ class ExportDataTest(MockEppLib):
|
|||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,dotgov@cisa.dhs.gov,Ready"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
|
|
|
@ -89,17 +89,230 @@ class LoggedInTests(TestWithUser):
|
|||
super().setUp()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
def test_home_lists_domain_applications(self):
|
||||
response = self.client.get("/")
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
|
||||
response = self.client.get("/")
|
||||
# count = 2 because it is also in screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=2)
|
||||
|
||||
# count = 7 because of screenreader content
|
||||
self.assertContains(response, "igorville.gov", count=7)
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_withdrawn_domain_application(self):
|
||||
"""Tests if the user can delete a DomainApplication in the 'withdrawn' status"""
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN
|
||||
)
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||
self.assertContains(home_page, "Delete")
|
||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_started_domain_application(self):
|
||||
"""Tests if the user can delete a DomainApplication in the 'started' status"""
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED
|
||||
)
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||
self.assertContains(home_page, "Delete")
|
||||
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_doesnt_delete_other_domain_applications(self):
|
||||
"""Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN"""
|
||||
|
||||
# Given that we are including a subset of items that can be deleted while excluding the rest,
|
||||
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
|
||||
draft_domain = DraftDomain.objects.create(name="igorville.gov")
|
||||
for status in DomainApplication.ApplicationStatus:
|
||||
if status not in [
|
||||
DomainApplication.ApplicationStatus.STARTED,
|
||||
DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
]:
|
||||
with self.subTest(status=status):
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user, requested_domain=draft_domain, status=status
|
||||
)
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(
|
||||
reverse("application-delete", kwargs={"pk": application.pk}), follow=True
|
||||
)
|
||||
|
||||
# Check for a 403 error - the end user should not be allowed to do this
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
desired_application = DomainApplication.objects.filter(requested_domain=draft_domain)
|
||||
|
||||
# Make sure the DomainApplication wasn't deleted
|
||||
self.assertEqual(desired_application.count(), 1)
|
||||
|
||||
# clean up
|
||||
application.delete()
|
||||
|
||||
def test_home_deletes_domain_application_and_orphans(self):
|
||||
"""Tests if delete for DomainApplication deletes orphaned Contact objects"""
|
||||
|
||||
# Create the site and contacts to delete (orphaned)
|
||||
contact = Contact.objects.create(
|
||||
first_name="Henry",
|
||||
last_name="Mcfakerson",
|
||||
)
|
||||
contact_shared = Contact.objects.create(
|
||||
first_name="Relative",
|
||||
last_name="Aether",
|
||||
)
|
||||
|
||||
# Create two non-orphaned contacts
|
||||
contact_2 = Contact.objects.create(
|
||||
first_name="Saturn",
|
||||
last_name="Mars",
|
||||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
authorizing_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
application.other_contacts.set([contact_2])
|
||||
|
||||
# Create a second application to attach contacts to
|
||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||
application_2 = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site_2,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
authorizing_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
application_2.other_contacts.set([contact_shared])
|
||||
|
||||
# Ensure that igorville.gov exists on the page
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||
|
||||
# igorville is now deleted
|
||||
self.assertNotContains(response, "igorville.gov")
|
||||
|
||||
# Check if the orphaned contact was deleted
|
||||
orphan = Contact.objects.filter(id=contact.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
# All non-orphan contacts should still exist and are unaltered
|
||||
try:
|
||||
current_user = Contact.objects.filter(id=contact_user.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
self.fail("contact_user (a non-orphaned contact) was deleted")
|
||||
|
||||
self.assertEqual(current_user, contact_user)
|
||||
try:
|
||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||
except Contact.DoesNotExist:
|
||||
self.fail("contact_2 (a non-orphaned contact) was deleted")
|
||||
|
||||
self.assertEqual(edge_case, contact_2)
|
||||
|
||||
def test_home_deletes_domain_application_and_shared_orphans(self):
|
||||
"""Test the edge case for an object that will become orphaned after a delete
|
||||
(but is not an orphan at the time of deletion)"""
|
||||
|
||||
# Create the site and contacts to delete (orphaned)
|
||||
contact = Contact.objects.create(
|
||||
first_name="Henry",
|
||||
last_name="Mcfakerson",
|
||||
)
|
||||
contact_shared = Contact.objects.create(
|
||||
first_name="Relative",
|
||||
last_name="Aether",
|
||||
)
|
||||
|
||||
# Create two non-orphaned contacts
|
||||
contact_2 = Contact.objects.create(
|
||||
first_name="Saturn",
|
||||
last_name="Mars",
|
||||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||
authorizing_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
application.other_contacts.set([contact_2])
|
||||
|
||||
# Create a second application to attach contacts to
|
||||
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||
application_2 = DomainApplication.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site_2,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
authorizing_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
application_2.other_contacts.set([contact_shared])
|
||||
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "teaville.gov")
|
||||
|
||||
# Trigger the delete logic
|
||||
response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True)
|
||||
|
||||
self.assertNotContains(response, "teaville.gov")
|
||||
|
||||
# Check if the orphaned contact was deleted
|
||||
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
def test_application_form_view(self):
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(
|
||||
|
|
|
@ -38,7 +38,7 @@ def write_row(writer, columns, domain_info):
|
|||
if security_contacts:
|
||||
security_email = security_contacts[0].email
|
||||
|
||||
invalid_emails = {"registrar@dotgov.gov"}
|
||||
invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"}
|
||||
# These are default emails that should not be displayed in the csv report
|
||||
if security_email is not None and security_email.lower() in invalid_emails:
|
||||
security_email = "(blank)"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
|
@ -10,8 +10,11 @@ from django.contrib import messages
|
|||
|
||||
from registrar.forms import application_wizard as forms
|
||||
from registrar.models import DomainApplication
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.utility import StrEnum
|
||||
from registrar.views.utility import StepsHelper
|
||||
from registrar.views.utility.permission_views import DomainApplicationPermissionDeleteView
|
||||
|
||||
from .utility import (
|
||||
DomainApplicationPermissionView,
|
||||
|
@ -83,13 +86,13 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
|
||||
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
||||
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"),
|
||||
Step.CURRENT_SITES: _("Current website for your organization"),
|
||||
Step.CURRENT_SITES: _("Current websites"),
|
||||
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
||||
Step.PURPOSE: _("Purpose of your domain"),
|
||||
Step.YOUR_CONTACT: _("Your contact information"),
|
||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||
Step.REVIEW: _("Review and submit your domain request"),
|
||||
}
|
||||
|
||||
|
@ -128,20 +131,26 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
if self._application:
|
||||
return self._application
|
||||
|
||||
# For linter. The else block should never be hit, but if it does,
|
||||
# there may be a UI consideration. That will need to be handled in another ticket.
|
||||
creator = None
|
||||
if self.request.user is not None and isinstance(self.request.user, User):
|
||||
creator = self.request.user
|
||||
else:
|
||||
raise ValueError("Invalid value for User")
|
||||
|
||||
if self.has_pk():
|
||||
id = self.storage["application_id"]
|
||||
try:
|
||||
self._application = DomainApplication.objects.get(
|
||||
creator=self.request.user, # type: ignore
|
||||
creator=creator,
|
||||
pk=id,
|
||||
)
|
||||
return self._application
|
||||
except DomainApplication.DoesNotExist:
|
||||
logger.debug("Application id %s did not have a DomainApplication" % id)
|
||||
|
||||
self._application = DomainApplication.objects.create(
|
||||
creator=self.request.user, # type: ignore
|
||||
)
|
||||
self._application = DomainApplication.objects.create(creator=self.request.user)
|
||||
|
||||
self.storage["application_id"] = self._application.id
|
||||
return self._application
|
||||
|
@ -150,7 +159,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
def storage(self):
|
||||
# marking session as modified on every access
|
||||
# so that updates to nested keys are always saved
|
||||
# push to sandbox will remove
|
||||
self.request.session.modified = True
|
||||
return self.request.session.setdefault(self.prefix, {})
|
||||
|
||||
|
@ -611,3 +619,102 @@ class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
|
|||
application.withdraw()
|
||||
application.save()
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
|
||||
class DomainApplicationDeleteView(DomainApplicationPermissionDeleteView):
|
||||
"""Delete view for home that allows the end user to delete DomainApplications"""
|
||||
|
||||
object: DomainApplication # workaround for type mismatch in DeleteView
|
||||
|
||||
def has_permission(self):
|
||||
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
||||
has_perm = super().has_permission()
|
||||
if not has_perm:
|
||||
return False
|
||||
|
||||
status = self.get_object().status
|
||||
valid_statuses = [DomainApplication.ApplicationStatus.WITHDRAWN, DomainApplication.ApplicationStatus.STARTED]
|
||||
if status not in valid_statuses:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_success_url(self):
|
||||
"""After a delete is successful, redirect to home"""
|
||||
return reverse("home")
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Grab all orphaned contacts
|
||||
application: DomainApplication = self.get_object()
|
||||
contacts_to_delete, duplicates = self._get_orphaned_contacts(application)
|
||||
|
||||
# Delete the DomainApplication
|
||||
response = super().post(request, *args, **kwargs)
|
||||
|
||||
# Delete orphaned contacts - but only for if they are not associated with a user
|
||||
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
||||
|
||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||
# This determines if any of these three fields share a contact, which is used for
|
||||
# the edge case where the same user may be an AO, and a submitter, for example.
|
||||
if len(duplicates) > 0:
|
||||
duplicates_to_delete, _ = self._get_orphaned_contacts(application, check_db=True)
|
||||
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
||||
|
||||
return response
|
||||
|
||||
def _get_orphaned_contacts(self, application: DomainApplication, check_db=False):
|
||||
"""
|
||||
Collects all orphaned contacts associated with a given DomainApplication object.
|
||||
|
||||
An orphaned contact is defined as a contact that is associated with the application,
|
||||
but not with any other application. This includes the authorizing official, the submitter,
|
||||
and any other contacts linked to the application.
|
||||
|
||||
Parameters:
|
||||
application (DomainApplication): The DomainApplication object for which to find orphaned contacts.
|
||||
check_db (bool, optional): A flag indicating whether to check the database for the existence of the contacts.
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing two lists. The first list contains the IDs of the orphaned contacts.
|
||||
The second list contains any duplicate contacts found. ([Contacts], [Contacts])
|
||||
"""
|
||||
contacts_to_delete = []
|
||||
|
||||
# Get each contact object on the DomainApplication object
|
||||
ao = application.authorizing_official
|
||||
submitter = application.submitter
|
||||
other_contacts = list(application.other_contacts.all())
|
||||
other_contact_ids = application.other_contacts.all().values_list("id", flat=True)
|
||||
|
||||
# Check if the desired item still exists in the DB
|
||||
if check_db:
|
||||
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None
|
||||
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
|
||||
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
||||
|
||||
# Pair each contact with its db related name for use in checking if it has joins
|
||||
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_applications")]
|
||||
checked_contacts.extend((contact, "contact_applications") for contact in other_contacts)
|
||||
|
||||
for contact, related_name in checked_contacts:
|
||||
if contact is not None and not contact.has_more_than_one_join(related_name):
|
||||
contacts_to_delete.append(contact.id)
|
||||
|
||||
return (contacts_to_delete, self._get_duplicates(checked_contacts))
|
||||
|
||||
def _get_contacts_by_id(self, contact_ids):
|
||||
"""Given a list of ids, grab contacts if it exists"""
|
||||
contacts = Contact.objects.filter(id__in=contact_ids)
|
||||
return contacts
|
||||
|
||||
def _get_duplicates(self, objects):
|
||||
"""Given a list of objects, return a list of which items were duplicates"""
|
||||
# Gets the occurence count
|
||||
object_dict = defaultdict(int)
|
||||
for contact, _related in objects:
|
||||
object_dict[contact] += 1
|
||||
|
||||
duplicates = [item for item, count in object_dict.items() if count > 1]
|
||||
return duplicates
|
||||
|
|
|
@ -568,7 +568,9 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
|||
"""The initial value for the form."""
|
||||
initial = super().get_initial()
|
||||
security_contact = self.object.security_contact
|
||||
if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov":
|
||||
|
||||
invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"]
|
||||
if security_contact is None or security_contact.email in invalid_emails:
|
||||
initial["security_email"] = None
|
||||
return initial
|
||||
initial["security_email"] = security_contact.email
|
||||
|
|
|
@ -7,15 +7,55 @@ def index(request):
|
|||
"""This page is available to anyone without logging in."""
|
||||
context = {}
|
||||
if request.user.is_authenticated:
|
||||
applications = DomainApplication.objects.filter(creator=request.user)
|
||||
# Get all domain applications the user has access to
|
||||
applications, deletable_applications = _get_applications(request)
|
||||
|
||||
context["domain_applications"] = applications
|
||||
|
||||
# Get all domains the user has access to
|
||||
domains = _get_domains(request)
|
||||
context["domains"] = domains
|
||||
|
||||
# Determine if the user will see applications that they can delete
|
||||
has_deletable_applications = deletable_applications.exists()
|
||||
context["has_deletable_applications"] = has_deletable_applications
|
||||
|
||||
# If they can delete applications, add the delete button to the context
|
||||
if has_deletable_applications:
|
||||
# Add the delete modal button to the context
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="delete-application">Yes, delete request</button>'
|
||||
)
|
||||
context["modal_button"] = modal_button
|
||||
|
||||
return render(request, "home.html", context)
|
||||
|
||||
|
||||
def _get_applications(request):
|
||||
"""Given the current request,
|
||||
get all DomainApplications that are associated with the UserDomainRole object.
|
||||
|
||||
Returns a tuple of all applications, and those that are deletable by the user.
|
||||
"""
|
||||
# Let's exclude the approved applications since our
|
||||
# domain_applications context will be used to populate
|
||||
# the active applications table
|
||||
context["domain_applications"] = applications.exclude(status="approved")
|
||||
applications = DomainApplication.objects.filter(creator=request.user).exclude(
|
||||
status=DomainApplication.ApplicationStatus.APPROVED
|
||||
)
|
||||
|
||||
# Create a placeholder DraftDomain for each incomplete draft
|
||||
valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
|
||||
deletable_applications = applications.filter(status__in=valid_statuses)
|
||||
|
||||
return (applications, deletable_applications)
|
||||
|
||||
|
||||
def _get_domains(request):
|
||||
"""Given the current request,
|
||||
get all domains that are associated with the UserDomainRole object"""
|
||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids)
|
||||
|
||||
context["domains"] = domains
|
||||
return render(request, "home.html", context)
|
||||
return Domain.objects.filter(id__in=domain_ids)
|
||||
|
|
|
@ -122,3 +122,11 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie
|
|||
|
||||
model = DomainInvitation
|
||||
object: DomainInvitation # workaround for type mismatch in DeleteView
|
||||
|
||||
|
||||
class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC):
|
||||
|
||||
"""Abstract view for deleting a DomainApplication."""
|
||||
|
||||
model = DomainApplication
|
||||
object: DomainApplication
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue