mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-01 15:34:53 +02:00
Merge branch 'main' of github.com:cisagov/manage.get.gov into es/476-add-cors-headers
This commit is contained in:
commit
adfcecacce
20 changed files with 743 additions and 124 deletions
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from django import forms
|
||||
from django.http import HttpResponse
|
||||
from django_fsm import get_available_FIELD_transitions
|
||||
from django.contrib import admin, messages
|
||||
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 registrar.models.domain import Domain
|
||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||
from registrar.utility import csv_export
|
||||
from . import models
|
||||
from auditlog.models import LogEntry # type: ignore
|
||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||
|
@ -747,8 +749,59 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
search_fields = ["name"]
|
||||
search_help_text = "Search by domain name."
|
||||
change_form_template = "django/admin/domain_change_form.html"
|
||||
change_list_template = "django/admin/domain_change_list.html"
|
||||
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):
|
||||
# Create dictionary of action functions
|
||||
ACTION_FUNCTIONS = {
|
||||
|
|
|
@ -273,31 +273,35 @@ function prepareDeleteButtons(formLabel) {
|
|||
// h2 and legend for DS form, label for nameservers
|
||||
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
||||
|
||||
// Ticket: 1192
|
||||
// if (isNameserversForm && index <= 1 && !node.innerHTML.includes('*')) {
|
||||
// // Create a new element
|
||||
// const newElement = document.createElement('abbr');
|
||||
// newElement.textContent = '*';
|
||||
// // TODO: finish building abbr
|
||||
// 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('*')) {
|
||||
// Create a new element
|
||||
const newElement = document.createElement('abbr');
|
||||
newElement.textContent = '*';
|
||||
newElement.setAttribute("title", "required");
|
||||
newElement.classList.add("usa-hint", "usa-hint--required");
|
||||
|
||||
// // Append the new element to the parent
|
||||
// node.appendChild(newElement);
|
||||
// // Find the next sibling that is an input element
|
||||
// let nextInputElement = node.nextElementSibling;
|
||||
// Append the new element to the label
|
||||
node.appendChild(newElement);
|
||||
// Find the next sibling that is an input element
|
||||
let nextInputElement = node.nextElementSibling;
|
||||
|
||||
// while (nextInputElement) {
|
||||
// if (nextInputElement.tagName === 'INPUT') {
|
||||
// // Found the next input element
|
||||
// console.log(nextInputElement);
|
||||
// break;
|
||||
// }
|
||||
// nextInputElement = nextInputElement.nextElementSibling;
|
||||
// }
|
||||
// nextInputElement.required = true;
|
||||
// }
|
||||
while (nextInputElement) {
|
||||
if (nextInputElement.tagName === 'INPUT') {
|
||||
// Found the next input element
|
||||
nextInputElement.setAttribute("required", "")
|
||||
break;
|
||||
}
|
||||
nextInputElement = nextInputElement.nextElementSibling;
|
||||
}
|
||||
nextInputElement.required = true;
|
||||
}
|
||||
|
||||
// Ticket: 1192 - remove if
|
||||
if (!(isNameserversForm && index <= 1)) {
|
||||
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}`);
|
||||
}
|
||||
|
@ -305,7 +309,15 @@ function prepareDeleteButtons(formLabel) {
|
|||
|
||||
// Display the add more button if we have less than 13 forms
|
||||
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";
|
||||
}
|
||||
|
||||
// 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
|
||||
prepareDeleteButtons(formLabel);
|
||||
|
||||
|
@ -348,6 +365,33 @@ function prepareDeleteButtons(formLabel) {
|
|||
// For the eample on Nameservers
|
||||
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++;
|
||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
||||
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
|
||||
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) {
|
||||
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);
|
||||
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_base_url = env.str("DJANGO_BASE_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_key = secret("DJANGO_SECRET_KEY")
|
||||
|
@ -382,8 +383,7 @@ LOGGING = {
|
|||
# each handler has its choice of format
|
||||
"formatters": {
|
||||
"verbose": {
|
||||
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] "
|
||||
"%(message)s",
|
||||
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
|
||||
"datefmt": "%d/%b/%Y %H:%M:%S",
|
||||
},
|
||||
"simple": {
|
||||
|
@ -494,11 +494,12 @@ OIDC_ALLOW_DYNAMIC_OP = False
|
|||
|
||||
# which provider to use if multiple are available
|
||||
# (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 = {
|
||||
"login.gov": {
|
||||
"identity sandbox": {
|
||||
"srv_discovery_url": "https://idp.int.identitysandbox.gov",
|
||||
"behaviour": {
|
||||
# the 'code' workflow requires direct connectivity from us to Login.gov
|
||||
|
@ -514,7 +515,26 @@ OIDC_PROVIDERS = {
|
|||
"token_endpoint_auth_method": ["private_key_jwt"],
|
||||
"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
|
||||
|
|
|
@ -23,11 +23,6 @@ class DomainAddUserForm(forms.Form):
|
|||
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):
|
||||
"""Form for changing nameservers."""
|
||||
|
||||
|
@ -35,7 +30,21 @@ class DomainNameserverForm(forms.Form):
|
|||
|
||||
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):
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
|
@ -44,6 +53,11 @@ class DomainNameserverForm(forms.Form):
|
|||
cleaned_data = super().clean()
|
||||
self.clean_empty_strings(cleaned_data)
|
||||
server = cleaned_data.get("server", "")
|
||||
# remove ANY spaces in the server field
|
||||
server = server.replace(" ", "")
|
||||
# lowercase the server
|
||||
server = server.lower()
|
||||
cleaned_data["server"] = server
|
||||
ip = cleaned_data.get("ip", None)
|
||||
# remove ANY spaces in the ip field
|
||||
ip = ip.replace(" ", "")
|
||||
|
@ -52,7 +66,7 @@ class DomainNameserverForm(forms.Form):
|
|||
ip_list = self.extract_ip_list(ip)
|
||||
|
||||
# validate if the form has a server or an ip
|
||||
if ip and ip_list or server:
|
||||
if (ip and ip_list) or server:
|
||||
self.validate_nameserver_ip_combo(domain, server, ip_list)
|
||||
|
||||
return cleaned_data
|
||||
|
|
|
@ -45,8 +45,10 @@ class Command(BaseCommand):
|
|||
self.transition_domains = TransitionDomain.objects.filter(
|
||||
email_sent=False,
|
||||
).order_by("username")
|
||||
logger.info("Found %d transition domains", len(self.transition_domains))
|
||||
|
||||
self.build_emails_to_send_array()
|
||||
logger.info("Prepared %d emails to send", len(self.emails_to_send))
|
||||
|
||||
if options["send_emails"]:
|
||||
logger.info("about to send emails")
|
||||
|
@ -58,6 +60,12 @@ class Command(BaseCommand):
|
|||
logger.info("done sending emails and updating transition_domains")
|
||||
else:
|
||||
logger.info("not sending emails")
|
||||
for email_context in self.emails_to_send:
|
||||
logger.info(
|
||||
"would send email to %s for %s",
|
||||
email_context["email"],
|
||||
email_context["domains"],
|
||||
)
|
||||
|
||||
def build_emails_to_send_array(self):
|
||||
"""this method sends emails to distinct usernames"""
|
||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
|||
import ipaddress
|
||||
import re
|
||||
from datetime import date
|
||||
from string import digits
|
||||
from typing import Optional
|
||||
|
||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||
|
@ -277,7 +276,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
return response.code
|
||||
except RegistryError as e:
|
||||
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
|
||||
raise e
|
||||
# OBJECT_EXISTS is an expected error code that should not raise
|
||||
# an exception, rather return the code to be handled separately
|
||||
if e.code == ErrorCode.OBJECT_EXISTS:
|
||||
return e.code
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _convert_list_to_dict(self, listToConvert: list[tuple[str, list]]):
|
||||
"""converts a list of hosts into a dictionary
|
||||
|
@ -305,30 +309,29 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
return bool(regex.match(nameserver))
|
||||
|
||||
@classmethod
|
||||
def isValidDomain(cls, nameserver: str):
|
||||
def isValidHost(cls, nameserver: str):
|
||||
"""Checks for validity of nameserver string based on these rules:
|
||||
- first character is alpha
|
||||
- last character is not - or .
|
||||
- all characters alpha, 0-9, -, or .
|
||||
- 2 character min, 24 character max (not including dashes/periods)
|
||||
- first character is alpha or digit
|
||||
- first and last character in each label is alpha or digit
|
||||
- all characters alpha (lowercase), digit, -, or .
|
||||
- each label has a min length of 1 and a max length of 63
|
||||
- total host name has a max length of 253
|
||||
"""
|
||||
# pattern to test for valid domain
|
||||
# pattern = r'^[a-zA-Z][a-zA-Z0-9-.]{0,22}[a-zA-Z0-9]$'
|
||||
pattern = r"^[a-zA-Z][a-zA-Z0-9-.]*(\.[a-zA-Z0-9-]+){2}[a-zA-Z0-9]$"
|
||||
# label pattern for each section of the host name, separated by .
|
||||
labelpattern = r"[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?"
|
||||
# lookahead pattern ensures first character not - and total length < 254
|
||||
lookaheadpatterns = r"^((?!-))(?=.{1,253}\.?$)"
|
||||
# pattern assembles lookaheadpatterns and ensures there are at least
|
||||
# 3 labels in the host name
|
||||
pattern = lookaheadpatterns + labelpattern + r"(\." + labelpattern + r"){2,}$"
|
||||
|
||||
# attempt to match the pattern
|
||||
match = re.match(pattern, nameserver)
|
||||
|
||||
# length of nameserver, not including - or .
|
||||
characters_to_exclude = "-."
|
||||
filtered_nameserver = "".join(
|
||||
char for char in nameserver if char not in characters_to_exclude
|
||||
)
|
||||
nameserverLength = len(filtered_nameserver)
|
||||
|
||||
# return true if nameserver matches, and length less than 25;
|
||||
# return true if nameserver matches
|
||||
# otherwise false
|
||||
return bool(match) and nameserverLength < 25
|
||||
return bool(match)
|
||||
|
||||
@classmethod
|
||||
def checkHostIPCombo(cls, name: str, nameserver: str, ip: list[str]):
|
||||
|
@ -349,7 +352,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
None"""
|
||||
if ip and not nameserver:
|
||||
raise NameserverError(code=nsErrorCodes.MISSING_HOST)
|
||||
elif nameserver and not cls.isValidDomain(nameserver):
|
||||
elif nameserver and not cls.isValidHost(nameserver):
|
||||
raise NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=nameserver)
|
||||
elif cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
|
||||
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
||||
|
@ -362,7 +365,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
for addr in ip:
|
||||
if not cls._valid_ip_addr(addr):
|
||||
raise NameserverError(
|
||||
code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip
|
||||
code=nsErrorCodes.INVALID_IP, nameserver=nameserver[:40], ip=ip
|
||||
)
|
||||
return None
|
||||
|
||||
|
@ -1228,34 +1231,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# ForeignKey on DomainInvitation creates an "invitations" member for
|
||||
# all of the invitations that have been sent for this domain
|
||||
|
||||
def _validate_host_tuples(self, hosts: list[tuple[str]]):
|
||||
"""
|
||||
Helper function. Validate hostnames and IP addresses.
|
||||
|
||||
Raises:
|
||||
ValueError if hostname or IP address appears invalid or mismatched.
|
||||
"""
|
||||
for host in hosts:
|
||||
hostname = host[0].lower()
|
||||
addresses: tuple[str] = host[1:] # type: ignore
|
||||
if not bool(Domain.HOST_REGEX.match(hostname)):
|
||||
raise ValueError("Invalid hostname: %s." % hostname)
|
||||
if len(hostname) > Domain.MAX_LENGTH:
|
||||
raise ValueError("Too long hostname: %s" % hostname)
|
||||
|
||||
is_subordinate = hostname.split(".", 1)[-1] == self.name
|
||||
if is_subordinate and len(addresses) == 0:
|
||||
raise ValueError(
|
||||
"Must supply IP addresses for subordinate host %s" % hostname
|
||||
)
|
||||
if not is_subordinate and len(addresses) > 0:
|
||||
raise ValueError("Must not supply IP addresses for %s" % hostname)
|
||||
|
||||
for address in addresses:
|
||||
allow = set(":." + digits)
|
||||
if any(c not in allow for c in address):
|
||||
raise ValueError("Invalid IP address: %s." % address)
|
||||
|
||||
def _get_or_create_domain(self):
|
||||
"""Try to fetch info about this domain. Create it if it does not exist."""
|
||||
already_tried_to_create = False
|
||||
|
@ -1625,7 +1600,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
return response.code
|
||||
except RegistryError as e:
|
||||
logger.error("Error _update_host, code was %s error was %s" % (e.code, e))
|
||||
raise e
|
||||
# OBJECT_EXISTS is an expected error code that should not raise
|
||||
# an exception, rather return the code to be handled separately
|
||||
if e.code == ErrorCode.OBJECT_EXISTS:
|
||||
return e.code
|
||||
else:
|
||||
raise e
|
||||
|
||||
def addAndRemoveHostsFromDomain(
|
||||
self, hostsToAdd: list[str], hostsToDelete: list[str]
|
||||
|
|
|
@ -11,10 +11,6 @@ class DomainHelper:
|
|||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
|
||||
|
||||
# a domain name is alphanumeric or hyphen, has at least 2 dots, doesn't
|
||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||
HOST_REGEX = re.compile(r"^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.){2,}([A-Za-z]){2,6}$")
|
||||
|
||||
# a domain can be no longer than 253 characters in total
|
||||
MAX_LENGTH = 253
|
||||
|
||||
|
|
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,8 +2,12 @@
|
|||
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
|
||||
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
|
||||
>
|
||||
{{ field.label }}
|
||||
{% if widget.attrs.required %}
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
{% endif %}
|
||||
{% if span_for_text %}
|
||||
<span>{{ field.label }}</span>
|
||||
{% else %}
|
||||
{{ field.label }}
|
||||
{% endif %}
|
||||
{% if widget.attrs.required %}
|
||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
{% endif %}
|
||||
</{{ label_tag }}>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<div class="usa-alert usa-alert--slim usa-alert--info">
|
||||
<div class="usa-alert__body">
|
||||
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is "example.gov" and your name server is "ns1.example.gov,” then an IP address is required.) To add multiple IP addresses, separate them with commas.</p>
|
||||
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is “example.gov” and your name server is “ns1.example.gov,” then an IP address is required.) To add multiple IP addresses, separate them with commas.</p>
|
||||
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,11 +35,14 @@
|
|||
{{ form.domain }}
|
||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||
{% 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 %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% input_with_errors form.server %}
|
||||
{% with span_for_text=True %}
|
||||
{% input_with_errors form.server %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
@ -49,14 +52,11 @@
|
|||
{% endwith %}
|
||||
</div>
|
||||
<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">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg><span class="margin-left-05">Delete</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<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">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg><span class="margin-left-05">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@
|
|||
type="submit"
|
||||
class="usa-button usa-button--outline"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1630,6 +1630,56 @@ class TestRegistrantNameservers(MockEppLib):
|
|||
return super().tearDown()
|
||||
|
||||
|
||||
class TestNameserverValidation(TestCase):
|
||||
"""Test the isValidDomain method which validates nameservers"""
|
||||
|
||||
def test_255_chars_is_too_long(self):
|
||||
"""Test that domain of 255 chars or longer is invalid"""
|
||||
domain_too_long = (
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
".bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
".bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
".bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
".bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
".bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.gov"
|
||||
)
|
||||
self.assertFalse(Domain.isValidHost(domain_too_long))
|
||||
|
||||
def test_64_char_label_too_long(self):
|
||||
"""Test that label of 64 characters or longer is invalid"""
|
||||
label_too_long = (
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
)
|
||||
domain_label_too_long = "www." + label_too_long + ".gov"
|
||||
self.assertFalse(Domain.isValidHost(domain_label_too_long))
|
||||
|
||||
def test_only_tld_and_sld(self):
|
||||
"""Test that host with only a tld and sld is invalid"""
|
||||
tld = "gov"
|
||||
sld = "example"
|
||||
domain_with_sld_and_tld = sld + "." + tld
|
||||
self.assertFalse(Domain.isValidHost(domain_with_sld_and_tld))
|
||||
|
||||
def test_improper_chars_in_nameserver(self):
|
||||
"""Test that host with improper chars is invalid"""
|
||||
invalid_chars = "*&^"
|
||||
domain_with_invalid_chars = "www.bad--" + invalid_chars + ".gov"
|
||||
self.assertFalse(Domain.isValidHost(domain_with_invalid_chars))
|
||||
|
||||
def test_misplaced_dashes(self):
|
||||
"""Test that misplaced dashes are invalid"""
|
||||
self.assertFalse(Domain.isValidHost("-www.example.gov"))
|
||||
self.assertFalse(Domain.isValidHost("www.example-.gov"))
|
||||
self.assertTrue(Domain.isValidHost("www.ex-ample.gov"))
|
||||
|
||||
def test_valid_hostname(self):
|
||||
"""Test that valid hostnames are valid"""
|
||||
self.assertTrue(Domain.isValidHost("www.tld.sld.gov"))
|
||||
self.assertTrue(Domain.isValidHost("www.valid.c"))
|
||||
self.assertTrue(Domain.isValidHost("2ww.valid.gov"))
|
||||
self.assertTrue(Domain.isValidHost("w.t.g"))
|
||||
|
||||
|
||||
class TestRegistrantDNSSEC(MockEppLib):
|
||||
"""Rule: Registrants may modify their secure DNS data"""
|
||||
|
||||
|
|
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"):
|
||||
self.domain.contacts.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
PublicContact.objects.all().delete()
|
||||
Domain.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
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# 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):
|
||||
"""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
|
||||
# error text appears four times, twice at the top of the page,
|
||||
# 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):
|
||||
|
@ -1833,7 +1844,11 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
(
|
||||
"RegistryError",
|
||||
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."),
|
||||
(
|
||||
|
@ -1868,7 +1883,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
self.assertEqual(len(messages), 1)
|
||||
message = messages[0]
|
||||
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):
|
||||
"""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 = {
|
||||
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
|
||||
"Update failed. Cannot contact the registry."
|
||||
),
|
||||
GenericErrorCodes.CANNOT_CONTACT_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."),
|
||||
}
|
||||
|
||||
|
@ -104,7 +106,9 @@ class NameserverError(Exception):
|
|||
NameserverErrorCodes.MISSING_HOST: (
|
||||
"Name server must be provided to enter IP address."
|
||||
),
|
||||
NameserverErrorCodes.INVALID_HOST: ("Name server, {}, is not valid."),
|
||||
NameserverErrorCodes.INVALID_HOST: (
|
||||
"Enter a name server in the required format, like ns1.example.com"
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, nameserver=None, ip=None, **kwargs):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue