mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-25 18:43:49 +02:00
Merge branch 'main' into dk/1016-nameservers-ui
This commit is contained in:
commit
3ff961cdac
15 changed files with 601 additions and 61 deletions
|
@ -4,7 +4,7 @@ applications:
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- python_buildpack
|
- python_buildpack
|
||||||
path: ../../src
|
path: ../../src
|
||||||
instances: 1
|
instances: 2
|
||||||
memory: 512M
|
memory: 512M
|
||||||
stack: cflinuxfs4
|
stack: cflinuxfs4
|
||||||
timeout: 180
|
timeout: 180
|
||||||
|
@ -23,6 +23,8 @@ applications:
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
|
# Which OIDC provider to use
|
||||||
|
OIDC_ACTIVE_PROVIDER: login.gov production
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-stable.app.cloud.gov
|
- route: getgov-stable.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -4,7 +4,7 @@ applications:
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- python_buildpack
|
- python_buildpack
|
||||||
path: ../../src
|
path: ../../src
|
||||||
instances: 1
|
instances: 2
|
||||||
memory: 512M
|
memory: 512M
|
||||||
stack: cflinuxfs4
|
stack: cflinuxfs4
|
||||||
timeout: 180
|
timeout: 180
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.http import HttpResponse
|
||||||
from django_fsm import get_available_FIELD_transitions
|
from django_fsm import get_available_FIELD_transitions
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
@ -10,6 +11,7 @@ from django.urls import reverse
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||||
|
from registrar.utility import csv_export
|
||||||
from . import models
|
from . import models
|
||||||
from auditlog.models import LogEntry # type: ignore
|
from auditlog.models import LogEntry # type: ignore
|
||||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||||
|
@ -747,8 +749,59 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
change_form_template = "django/admin/domain_change_form.html"
|
change_form_template = "django/admin/domain_change_form.html"
|
||||||
|
change_list_template = "django/admin/domain_change_list.html"
|
||||||
readonly_fields = ["state"]
|
readonly_fields = ["state"]
|
||||||
|
|
||||||
|
def export_data_type(self, request):
|
||||||
|
# match the CSV example with all the fields
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
||||||
|
csv_export.export_data_type_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def export_data_full(self, request):
|
||||||
|
# Smaller export based on 1
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
||||||
|
csv_export.export_data_full_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def export_data_federal(self, request):
|
||||||
|
# Federal only
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
||||||
|
csv_export.export_data_federal_to_csv(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
urlpatterns = super().get_urls()
|
||||||
|
|
||||||
|
# Used to extrapolate a path name, for instance
|
||||||
|
# name="{app_label}_{model_name}_export_data_type"
|
||||||
|
info = self.model._meta.app_label, self.model._meta.model_name
|
||||||
|
|
||||||
|
my_url = [
|
||||||
|
path(
|
||||||
|
"export_data_type/",
|
||||||
|
self.export_data_type,
|
||||||
|
name="%s_%s_export_data_type" % info,
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"export_data_full/",
|
||||||
|
self.export_data_full,
|
||||||
|
name="%s_%s_export_data_full" % info,
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"export_data_federal/",
|
||||||
|
self.export_data_federal,
|
||||||
|
name="%s_%s_export_data_federal" % info,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return my_url + urlpatterns
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
# Create dictionary of action functions
|
# Create dictionary of action functions
|
||||||
ACTION_FUNCTIONS = {
|
ACTION_FUNCTIONS = {
|
||||||
|
|
|
@ -273,31 +273,35 @@ function prepareDeleteButtons(formLabel) {
|
||||||
// h2 and legend for DS form, label for nameservers
|
// h2 and legend for DS form, label for nameservers
|
||||||
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
||||||
|
|
||||||
// Ticket: 1192
|
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
||||||
// if (isNameserversForm && index <= 1 && !node.innerHTML.includes('*')) {
|
// inject the USWDS required markup and make sure the INPUT is required
|
||||||
// // Create a new element
|
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
||||||
// const newElement = document.createElement('abbr');
|
// Create a new element
|
||||||
// newElement.textContent = '*';
|
const newElement = document.createElement('abbr');
|
||||||
// // TODO: finish building abbr
|
newElement.textContent = '*';
|
||||||
|
newElement.setAttribute("title", "required");
|
||||||
|
newElement.classList.add("usa-hint", "usa-hint--required");
|
||||||
|
|
||||||
// // Append the new element to the parent
|
// Append the new element to the label
|
||||||
// node.appendChild(newElement);
|
node.appendChild(newElement);
|
||||||
// // Find the next sibling that is an input element
|
// Find the next sibling that is an input element
|
||||||
// let nextInputElement = node.nextElementSibling;
|
let nextInputElement = node.nextElementSibling;
|
||||||
|
|
||||||
// while (nextInputElement) {
|
while (nextInputElement) {
|
||||||
// if (nextInputElement.tagName === 'INPUT') {
|
if (nextInputElement.tagName === 'INPUT') {
|
||||||
// // Found the next input element
|
// Found the next input element
|
||||||
// console.log(nextInputElement);
|
nextInputElement.setAttribute("required", "")
|
||||||
// break;
|
break;
|
||||||
// }
|
}
|
||||||
// nextInputElement = nextInputElement.nextElementSibling;
|
nextInputElement = nextInputElement.nextElementSibling;
|
||||||
// }
|
}
|
||||||
// nextInputElement.required = true;
|
nextInputElement.required = true;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Ticket: 1192 - remove if
|
let innerSpan = node.querySelector('span')
|
||||||
if (!(isNameserversForm && index <= 1)) {
|
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(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||||
}
|
}
|
||||||
|
@ -305,7 +309,15 @@ function prepareDeleteButtons(formLabel) {
|
||||||
|
|
||||||
// Display the add more button if we have less than 13 forms
|
// Display the add more button if we have less than 13 forms
|
||||||
if (isNameserversForm && forms.length <= 13) {
|
if (isNameserversForm && forms.length <= 13) {
|
||||||
addButton.classList.remove("display-none")
|
console.log('remove disabled');
|
||||||
|
addButton.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNameserversForm && forms.length < 3) {
|
||||||
|
// Hide the delete buttons on the remaining nameservers
|
||||||
|
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||||
|
deleteButton.setAttribute("disabled", "true");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -333,6 +345,11 @@ function prepareDeleteButtons(formLabel) {
|
||||||
formLabel = "DS Data record";
|
formLabel = "DS Data record";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On load: Disable the add more button if we have 13 forms
|
||||||
|
if (isNameserversForm && document.querySelectorAll(".repeatable-form").length == 13) {
|
||||||
|
addButton.setAttribute("disabled", "true");
|
||||||
|
}
|
||||||
|
|
||||||
// Attach click event listener on the delete buttons of the existing forms
|
// Attach click event listener on the delete buttons of the existing forms
|
||||||
prepareDeleteButtons(formLabel);
|
prepareDeleteButtons(formLabel);
|
||||||
|
|
||||||
|
@ -348,6 +365,33 @@ function prepareDeleteButtons(formLabel) {
|
||||||
// For the eample on Nameservers
|
// For the eample on Nameservers
|
||||||
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
||||||
|
|
||||||
|
// Some Nameserver form checks since the delete can mess up the source object we're copying
|
||||||
|
// in regards to required fields and hidden delete buttons
|
||||||
|
if (isNameserversForm) {
|
||||||
|
|
||||||
|
// If the source element we're copying has required on an input,
|
||||||
|
// reset that input
|
||||||
|
let formRequiredNeedsCleanUp = newForm.innerHTML.includes('*');
|
||||||
|
if (formRequiredNeedsCleanUp) {
|
||||||
|
newForm.querySelector('label abbr').remove();
|
||||||
|
// Get all input elements within the container
|
||||||
|
const inputElements = newForm.querySelectorAll("input");
|
||||||
|
// Loop through each input element and remove the 'required' attribute
|
||||||
|
inputElements.forEach((input) => {
|
||||||
|
if (input.hasAttribute("required")) {
|
||||||
|
input.removeAttribute("required");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source element we're copying has an disabled delete button,
|
||||||
|
// enable that button
|
||||||
|
let deleteButton= newForm.querySelector('.delete-record');
|
||||||
|
if (deleteButton.hasAttribute("disabled")) {
|
||||||
|
deleteButton.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
formNum++;
|
formNum++;
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||||
|
@ -397,9 +441,18 @@ function prepareDeleteButtons(formLabel) {
|
||||||
// Attach click event listener on the delete buttons of the new form
|
// Attach click event listener on the delete buttons of the new form
|
||||||
prepareDeleteButtons(formLabel);
|
prepareDeleteButtons(formLabel);
|
||||||
|
|
||||||
// Hide the add more button if we have 13 forms
|
// Disable the add more button if we have 13 forms
|
||||||
if (isNameserversForm && formNum == 13) {
|
if (isNameserversForm && formNum == 13) {
|
||||||
addButton.classList.add("display-none")
|
addButton.setAttribute("disabled", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNameserversForm && forms.length >= 2) {
|
||||||
|
// Enable the delete buttons on the nameservers
|
||||||
|
forms.forEach((form, index) => {
|
||||||
|
Array.from(form.querySelectorAll('.delete-record')).forEach((deleteButton) => {
|
||||||
|
deleteButton.removeAttribute("disabled");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -180,3 +180,48 @@ h1, h2, h3 {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
color: var(--header-link-color);
|
color: var(--header-link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Font mismatch issue due to conflicts between django and uswds,
|
||||||
|
// rough overrides for consistency and readability. May want to revise
|
||||||
|
// in the future
|
||||||
|
.object-tools li a,
|
||||||
|
.object-tools p a {
|
||||||
|
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||||
|
text-transform: capitalize!important;
|
||||||
|
font-size: 14px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For consistency, make the overrided p a
|
||||||
|
// object tool buttons the same size as the ul li a
|
||||||
|
.object-tools p {
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix margins in mobile view
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.object-tools li {
|
||||||
|
// our CSS is read before django's, so need !important
|
||||||
|
// to override
|
||||||
|
margin-left: 0!important;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix height of buttons
|
||||||
|
.object-tools li {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixing height of buttons breaks layout because
|
||||||
|
// object-tools and changelist are siblings with
|
||||||
|
// flexbox positioning
|
||||||
|
#changelist {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account for the h2, roughly 90px
|
||||||
|
@include at-media(tablet) {
|
||||||
|
.object-tools {
|
||||||
|
padding-left: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ env_debug = env.bool("DJANGO_DEBUG", default=False)
|
||||||
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
||||||
env_base_url = env.str("DJANGO_BASE_URL")
|
env_base_url = env.str("DJANGO_BASE_URL")
|
||||||
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
||||||
|
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
|
||||||
|
|
||||||
secret_login_key = b64decode(secret("DJANGO_SECRET_LOGIN_KEY", ""))
|
secret_login_key = b64decode(secret("DJANGO_SECRET_LOGIN_KEY", ""))
|
||||||
secret_key = secret("DJANGO_SECRET_KEY")
|
secret_key = secret("DJANGO_SECRET_KEY")
|
||||||
|
@ -370,8 +371,7 @@ LOGGING = {
|
||||||
# each handler has its choice of format
|
# each handler has its choice of format
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"verbose": {
|
"verbose": {
|
||||||
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] "
|
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
|
||||||
"%(message)s",
|
|
||||||
"datefmt": "%d/%b/%Y %H:%M:%S",
|
"datefmt": "%d/%b/%Y %H:%M:%S",
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
|
@ -482,11 +482,12 @@ OIDC_ALLOW_DYNAMIC_OP = False
|
||||||
|
|
||||||
# which provider to use if multiple are available
|
# which provider to use if multiple are available
|
||||||
# (code does not currently support user selection)
|
# (code does not currently support user selection)
|
||||||
OIDC_ACTIVE_PROVIDER = "login.gov"
|
# See above for the default value if the env variable is missing
|
||||||
|
OIDC_ACTIVE_PROVIDER = env_oidc_active_provider
|
||||||
|
|
||||||
|
|
||||||
OIDC_PROVIDERS = {
|
OIDC_PROVIDERS = {
|
||||||
"login.gov": {
|
"identity sandbox": {
|
||||||
"srv_discovery_url": "https://idp.int.identitysandbox.gov",
|
"srv_discovery_url": "https://idp.int.identitysandbox.gov",
|
||||||
"behaviour": {
|
"behaviour": {
|
||||||
# the 'code' workflow requires direct connectivity from us to Login.gov
|
# the 'code' workflow requires direct connectivity from us to Login.gov
|
||||||
|
@ -502,7 +503,26 @@ OIDC_PROVIDERS = {
|
||||||
"token_endpoint_auth_method": ["private_key_jwt"],
|
"token_endpoint_auth_method": ["private_key_jwt"],
|
||||||
"sp_private_key": secret_login_key,
|
"sp_private_key": secret_login_key,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
"login.gov production": {
|
||||||
|
"srv_discovery_url": "https://secure.login.gov",
|
||||||
|
"behaviour": {
|
||||||
|
# the 'code' workflow requires direct connectivity from us to Login.gov
|
||||||
|
"response_type": "code",
|
||||||
|
"scope": ["email", "profile:name", "phone"],
|
||||||
|
"user_info_request": ["email", "first_name", "last_name", "phone"],
|
||||||
|
"acr_value": "http://idmanagement.gov/ns/assurance/ial/2",
|
||||||
|
},
|
||||||
|
"client_registration": {
|
||||||
|
"client_id": (
|
||||||
|
"urn:gov:cisa:openidconnect.profiles:sp:sso:cisa:dotgov_registrar"
|
||||||
|
),
|
||||||
|
"redirect_uris": [f"{env_base_url}/openid/callback/login/"],
|
||||||
|
"post_logout_redirect_uris": [f"{env_base_url}/openid/callback/logout/"],
|
||||||
|
"token_endpoint_auth_method": ["private_key_jwt"],
|
||||||
|
"sp_private_key": secret_login_key,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -23,11 +23,6 @@ class DomainAddUserForm(forms.Form):
|
||||||
email = forms.EmailField(label="Email")
|
email = forms.EmailField(label="Email")
|
||||||
|
|
||||||
|
|
||||||
class IPAddressField(forms.CharField):
|
|
||||||
def validate(self, value):
|
|
||||||
super().validate(value) # Run the default CharField validation
|
|
||||||
|
|
||||||
|
|
||||||
class DomainNameserverForm(forms.Form):
|
class DomainNameserverForm(forms.Form):
|
||||||
"""Form for changing nameservers."""
|
"""Form for changing nameservers."""
|
||||||
|
|
||||||
|
@ -35,7 +30,21 @@ class DomainNameserverForm(forms.Form):
|
||||||
|
|
||||||
server = forms.CharField(label="Name server", strip=True)
|
server = forms.CharField(label="Name server", strip=True)
|
||||||
|
|
||||||
ip = forms.CharField(label="IP Address (IPv4 or IPv6)", strip=True, required=False)
|
ip = forms.CharField(
|
||||||
|
label="IP address (IPv4 or IPv6)",
|
||||||
|
strip=True,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(DomainNameserverForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# add custom error messages
|
||||||
|
self.fields["server"].error_messages.update(
|
||||||
|
{
|
||||||
|
"required": "A minimum of 2 name servers are required.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
# clean is called from clean_forms, which is called from is_valid
|
# clean is called from clean_forms, which is called from is_valid
|
||||||
|
|
23
src/registrar/templates/django/admin/domain_change_list.html
Normal file
23
src/registrar/templates/django/admin/domain_change_list.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "admin/change_list.html" %}
|
||||||
|
|
||||||
|
{% block object-tools %}
|
||||||
|
|
||||||
|
<ul class="object-tools">
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domain_export_data_type' %}" class="button">Export all domain metadata</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domain_export_data_full' %}" class="button">Export current-full.csv</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domain_export_data_federal' %}" class="button">Export current-federal.csv</a>
|
||||||
|
</li>
|
||||||
|
{% if has_add_permission %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:registrar_domain_add' %}" class="addlink">
|
||||||
|
Add Domain
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
|
@ -2,7 +2,11 @@
|
||||||
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
|
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
|
||||||
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
|
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
|
||||||
>
|
>
|
||||||
|
{% if span_for_text %}
|
||||||
|
<span>{{ field.label }}</span>
|
||||||
|
{% else %}
|
||||||
{{ field.label }}
|
{{ field.label }}
|
||||||
|
{% endif %}
|
||||||
{% if widget.attrs.required %}
|
{% if widget.attrs.required %}
|
||||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<h1>DNSSEC</h1>
|
<h1>DNSSEC</h1>
|
||||||
|
|
||||||
<p>DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
<p>DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
||||||
|
|
||||||
<form class="usa-form usa-form--text-width" method="post">
|
<form class="usa-form usa-form--text-width" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
|
@ -35,11 +35,14 @@
|
||||||
{{ form.domain }}
|
{{ form.domain }}
|
||||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||||
{% if forloop.counter <= 2 %}
|
{% if forloop.counter <= 2 %}
|
||||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
{# span_for_text will wrap the copy in s <span>, which we'll use in the JS for this component #}
|
||||||
|
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" span_for_text=True %}
|
||||||
{% input_with_errors form.server %}
|
{% input_with_errors form.server %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% with span_for_text=True %}
|
||||||
{% input_with_errors form.server %}
|
{% input_with_errors form.server %}
|
||||||
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,14 +52,11 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tablet:grid-col-2">
|
<div class="tablet:grid-col-2">
|
||||||
{% comment %} TODO: remove this if for 1192 {% endcomment %}
|
|
||||||
{% if forloop.counter > 2 %}
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block delete-record margin-bottom-075">
|
<button type="button" class="usa-button usa-button--unstyled display-block delete-record margin-bottom-075">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg><span class="margin-left-05">Delete</span>
|
</svg><span class="margin-left-05">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
type="submit"
|
type="submit"
|
||||||
class="usa-button usa-button--outline"
|
class="usa-button usa-button--outline"
|
||||||
name="btn-cancel-click"
|
name="btn-cancel-click"
|
||||||
aria-label="Reset the data in the Name Server form to the registry state (undo changes)"
|
aria-label="Reset the data in the name server form to the registry state (undo changes)"
|
||||||
>Cancel
|
>Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
195
src/registrar/tests/test_reports.py
Normal file
195
src/registrar/tests/test_reports.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from io import StringIO
|
||||||
|
import csv
|
||||||
|
from registrar.models.domain_information import DomainInformation
|
||||||
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.user import User
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from registrar.utility.csv_export import export_domains_to_writer
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
username = "test_user"
|
||||||
|
first_name = "First"
|
||||||
|
last_name = "Last"
|
||||||
|
email = "info@example.com"
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username=username, first_name=first_name, last_name=last_name, email=email
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_1, _ = Domain.objects.get_or_create(
|
||||||
|
name="cdomain1.gov", state=Domain.State.READY
|
||||||
|
)
|
||||||
|
self.domain_2, _ = Domain.objects.get_or_create(
|
||||||
|
name="adomain2.gov", state=Domain.State.DNS_NEEDED
|
||||||
|
)
|
||||||
|
self.domain_3, _ = Domain.objects.get_or_create(
|
||||||
|
name="ddomain3.gov", state=Domain.State.ON_HOLD
|
||||||
|
)
|
||||||
|
self.domain_4, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain4.gov", state=Domain.State.UNKNOWN
|
||||||
|
)
|
||||||
|
self.domain_4, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain4.gov", state=Domain.State.UNKNOWN
|
||||||
|
)
|
||||||
|
|
||||||
|
self.domain_information_1, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_1,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="World War I Centennial Commission",
|
||||||
|
federal_type="executive",
|
||||||
|
)
|
||||||
|
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_2,
|
||||||
|
organization_type="interstate",
|
||||||
|
)
|
||||||
|
self.domain_information_3, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_3,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
)
|
||||||
|
self.domain_information_4, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_4,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def test_export_domains_to_writer(self):
|
||||||
|
"""Test that export_domains_to_writer returns the
|
||||||
|
existing domain, test that sort by domain name works,
|
||||||
|
test that filter works"""
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
|
# Define columns, sort fields, and filter condition
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
"Agency",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State",
|
||||||
|
"AO",
|
||||||
|
"AO email",
|
||||||
|
"Submitter",
|
||||||
|
"Submitter title",
|
||||||
|
"Submitter email",
|
||||||
|
"Submitter phone",
|
||||||
|
"Security Contact Email",
|
||||||
|
"Status",
|
||||||
|
]
|
||||||
|
sort_fields = ["domain__name"]
|
||||||
|
filter_condition = {
|
||||||
|
"domain__state__in": [
|
||||||
|
Domain.State.READY,
|
||||||
|
Domain.State.DNS_NEEDED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the export function
|
||||||
|
export_domains_to_writer(writer, columns, sort_fields, filter_condition)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
|
||||||
|
# We expect READY domains,
|
||||||
|
# sorted alphabetially by domain name
|
||||||
|
expected_content = (
|
||||||
|
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||||
|
"AO email,Submitter,Submitter title,Submitter email,Submitter phone,"
|
||||||
|
"Security Contact Email,Status\n"
|
||||||
|
"adomain2.gov,Interstate,dnsneeded\n"
|
||||||
|
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,ready\n"
|
||||||
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,onhold\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
|
csv_content = (
|
||||||
|
csv_content.replace(",,", "")
|
||||||
|
.replace(",", "")
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace("\r\n", "\n")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
expected_content = (
|
||||||
|
expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
def test_export_domains_to_writer_additional(self):
|
||||||
|
"""An additional test for filters and multi-column sort"""
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
|
# Define columns, sort fields, and filter condition
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
"Agency",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State",
|
||||||
|
"Security Contact Email",
|
||||||
|
]
|
||||||
|
sort_fields = ["domain__name", "federal_agency", "organization_type"]
|
||||||
|
filter_condition = {
|
||||||
|
"organization_type__icontains": "federal",
|
||||||
|
"domain__state__in": [
|
||||||
|
Domain.State.READY,
|
||||||
|
Domain.State.DNS_NEEDED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the export function
|
||||||
|
export_domains_to_writer(writer, columns, sort_fields, filter_condition)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
|
||||||
|
# We expect READY domains,
|
||||||
|
# federal only
|
||||||
|
# sorted alphabetially by domain name
|
||||||
|
expected_content = (
|
||||||
|
"Domain name,Domain type,Agency,Organization name,City,"
|
||||||
|
"State,Security Contact Email\n"
|
||||||
|
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
|
||||||
|
"ddomain3.gov,Federal,Armed Forces Retirement Home\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
|
csv_content = (
|
||||||
|
csv_content.replace(",,", "")
|
||||||
|
.replace(",", "")
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace("\r\n", "\n")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
expected_content = (
|
||||||
|
expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(csv_content, expected_content)
|
|
@ -1170,6 +1170,7 @@ class TestWithDomainPermissions(TestWithUser):
|
||||||
if hasattr(self.domain, "contacts"):
|
if hasattr(self.domain, "contacts"):
|
||||||
self.domain.contacts.all().delete()
|
self.domain.contacts.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
PublicContact.objects.all().delete()
|
PublicContact.objects.all().delete()
|
||||||
Domain.objects.all().delete()
|
Domain.objects.all().delete()
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
@ -1464,7 +1465,12 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
# form submission was a post with an error, response should be a 200
|
# form submission was a post with an error, response should be a 200
|
||||||
# error text appears twice, once at the top of the page, once around
|
# error text appears twice, once at the top of the page, once around
|
||||||
# the required field. form requires a minimum of 2 name servers
|
# the required field. form requires a minimum of 2 name servers
|
||||||
self.assertContains(result, "This field is required.", count=2, status_code=200)
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
"A minimum of 2 name servers are required.",
|
||||||
|
count=2,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
def test_domain_nameservers_form_submit_subdomain_missing_ip(self):
|
def test_domain_nameservers_form_submit_subdomain_missing_ip(self):
|
||||||
"""Nameserver form catches missing ip error on subdomain.
|
"""Nameserver form catches missing ip error on subdomain.
|
||||||
|
@ -1665,7 +1671,12 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
# form submission was a post with an error, response should be a 200
|
# form submission was a post with an error, response should be a 200
|
||||||
# error text appears four times, twice at the top of the page,
|
# error text appears four times, twice at the top of the page,
|
||||||
# once around each required field.
|
# once around each required field.
|
||||||
self.assertContains(result, "This field is required", count=4, status_code=200)
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
"A minimum of 2 name servers are required.",
|
||||||
|
count=4,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainAuthorizingOfficial(TestDomainOverview):
|
class TestDomainAuthorizingOfficial(TestDomainOverview):
|
||||||
|
@ -1833,7 +1844,11 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
||||||
(
|
(
|
||||||
"RegistryError",
|
"RegistryError",
|
||||||
form_data_registry_error,
|
form_data_registry_error,
|
||||||
"Update failed. Cannot contact the registry.",
|
"""
|
||||||
|
We’re experiencing a system connection error. Please wait a few minutes
|
||||||
|
and try again. If you continue to receive this error after a few tries,
|
||||||
|
contact help@get.gov
|
||||||
|
""",
|
||||||
),
|
),
|
||||||
("ContactError", form_data_contact_error, "Value entered was wrong."),
|
("ContactError", form_data_contact_error, "Value entered was wrong."),
|
||||||
(
|
(
|
||||||
|
@ -1868,7 +1883,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
||||||
self.assertEqual(len(messages), 1)
|
self.assertEqual(len(messages), 1)
|
||||||
message = messages[0]
|
message = messages[0]
|
||||||
self.assertEqual(message.tags, message_tag)
|
self.assertEqual(message.tags, message_tag)
|
||||||
self.assertEqual(message.message, expected_message)
|
self.assertEqual(message.message.strip(), expected_message.strip())
|
||||||
|
|
||||||
def test_domain_overview_blocked_for_ineligible_user(self):
|
def test_domain_overview_blocked_for_ineligible_user(self):
|
||||||
"""We could easily duplicate this test for all domain management
|
"""We could easily duplicate this test for all domain management
|
||||||
|
|
119
src/registrar/utility/csv_export.py
Normal file
119
src/registrar/utility/csv_export.py
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import csv
|
||||||
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.domain_information import DomainInformation
|
||||||
|
from registrar.models.public_contact import PublicContact
|
||||||
|
|
||||||
|
|
||||||
|
def export_domains_to_writer(writer, columns, sort_fields, filter_condition):
|
||||||
|
# write columns headers to writer
|
||||||
|
writer.writerow(columns)
|
||||||
|
|
||||||
|
domainInfos = DomainInformation.objects.filter(**filter_condition).order_by(
|
||||||
|
*sort_fields
|
||||||
|
)
|
||||||
|
for domainInfo in domainInfos:
|
||||||
|
security_contacts = domainInfo.domain.contacts.filter(
|
||||||
|
contact_type=PublicContact.ContactTypeChoices.SECURITY
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a dictionary of fields which can be included in output
|
||||||
|
FIELDS = {
|
||||||
|
"Domain name": domainInfo.domain.name,
|
||||||
|
"Domain type": domainInfo.get_organization_type_display()
|
||||||
|
+ " - "
|
||||||
|
+ domainInfo.get_federal_type_display()
|
||||||
|
if domainInfo.federal_type
|
||||||
|
else domainInfo.get_organization_type_display(),
|
||||||
|
"Agency": domainInfo.federal_agency,
|
||||||
|
"Organization name": domainInfo.organization_name,
|
||||||
|
"City": domainInfo.city,
|
||||||
|
"State": domainInfo.state_territory,
|
||||||
|
"AO": domainInfo.authorizing_official.first_name
|
||||||
|
+ " "
|
||||||
|
+ domainInfo.authorizing_official.last_name
|
||||||
|
if domainInfo.authorizing_official
|
||||||
|
else " ",
|
||||||
|
"AO email": domainInfo.authorizing_official.email
|
||||||
|
if domainInfo.authorizing_official
|
||||||
|
else " ",
|
||||||
|
"Security Contact Email": security_contacts[0].email
|
||||||
|
if security_contacts
|
||||||
|
else " ",
|
||||||
|
"Status": domainInfo.domain.state,
|
||||||
|
"Expiration Date": domainInfo.domain.expiration_date,
|
||||||
|
}
|
||||||
|
writer.writerow([FIELDS.get(column, "") for column in columns])
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_type_to_csv(csv_file):
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# define columns to include in export
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
"Agency",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State",
|
||||||
|
"AO",
|
||||||
|
"AO email",
|
||||||
|
"Security Contact Email",
|
||||||
|
"Status",
|
||||||
|
"Expiration Date",
|
||||||
|
]
|
||||||
|
sort_fields = ["domain__name"]
|
||||||
|
filter_condition = {
|
||||||
|
"domain__state__in": [
|
||||||
|
Domain.State.READY,
|
||||||
|
Domain.State.DNS_NEEDED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
export_domains_to_writer(writer, columns, sort_fields, filter_condition)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_full_to_csv(csv_file):
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# define columns to include in export
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
"Agency",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State",
|
||||||
|
"Security Contact Email",
|
||||||
|
]
|
||||||
|
sort_fields = ["domain__name", "federal_agency", "organization_type"]
|
||||||
|
filter_condition = {
|
||||||
|
"domain__state__in": [
|
||||||
|
Domain.State.READY,
|
||||||
|
Domain.State.DNS_NEEDED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
export_domains_to_writer(writer, columns, sort_fields, filter_condition)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_federal_to_csv(csv_file):
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# define columns to include in export
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
"Agency",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State",
|
||||||
|
"Security Contact Email",
|
||||||
|
]
|
||||||
|
sort_fields = ["domain__name", "federal_agency", "organization_type"]
|
||||||
|
filter_condition = {
|
||||||
|
"organization_type__icontains": "federal",
|
||||||
|
"domain__state__in": [
|
||||||
|
Domain.State.READY,
|
||||||
|
Domain.State.DNS_NEEDED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
export_domains_to_writer(writer, columns, sort_fields, filter_condition)
|
|
@ -39,9 +39,11 @@ class GenericError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_error_mapping = {
|
_error_mapping = {
|
||||||
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
|
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: """
|
||||||
"Update failed. Cannot contact the registry."
|
We’re experiencing a system connection error. Please wait a few minutes
|
||||||
),
|
and try again. If you continue to receive this error after a few tries,
|
||||||
|
contact help@get.gov
|
||||||
|
""",
|
||||||
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue