Merge branch 'nl/981-test-domain-migration-script' of https://github.com/cisagov/manage.get.gov into nl/981-test-domain-migration-script

This commit is contained in:
zandercymatics 2023-11-02 12:17:13 -06:00
commit 5afc534370
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
17 changed files with 618 additions and 78 deletions

View file

@ -4,7 +4,7 @@ applications:
buildpacks:
- python_buildpack
path: ../../src
instances: 1
instances: 2
memory: 512M
stack: cflinuxfs4
timeout: 180
@ -23,6 +23,8 @@ applications:
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
# Which OIDC provider to use
OIDC_ACTIVE_PROVIDER: login.gov production
routes:
- route: getgov-stable.app.cloud.gov
services:

View file

@ -4,7 +4,7 @@ applications:
buildpacks:
- python_buildpack
path: ../../src
instances: 1
instances: 2
memory: 512M
stack: cflinuxfs4
timeout: 180

View file

@ -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 = {

View file

@ -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");
});
});
}
}
})();

View file

@ -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;
}
}

View file

@ -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")
@ -370,8 +371,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": {
@ -482,11 +482,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
@ -502,7 +503,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

View file

@ -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

View 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 %}

View file

@ -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 }}>

View file

@ -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>

View file

@ -1,7 +1,7 @@
TESTUSER|52563_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||testuser@gmail.com|GSA|VERISIGN|ctldbatch|2021-06-30T17:58:09Z|VERISIGN|ctldbatch|2021-06-30T18:18:09Z|
RJD1|52545_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||agustina.wyman7@test.com|GSA|VERISIGN|ctldbatch|2021-06-29T18:53:09Z|VERISIGN|ctldbatch|2021-06-29T18:58:08Z|
JAKING|52555_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||susy.martin4@test.com|GSA|VERISIGN|ctldbatch|2021-06-30T15:23:10Z|VERISIGN|ctldbatch|2021-06-30T15:38:10Z|
JBOONE|52556_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||stephania.winters4@test.com|GSA|VERISIGN|ctldbatch|2021-06-30T15:23:10Z|VERISIGN|ctldbatch|2021-06-30T18:28:09Z|
MKELLEY|52557_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||alexandra.bobbitt5@test.com|GSA|VERISIGN|ctldbatch|2021-06-30T15:23:10Z|VERISIGN|ctldbatch|2021-08-02T22:13:09Z|
CWILSON|52562_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||jospeh.mcdowell3@test.com|GSA|VERISIGN|ctldbatch|2021-06-30T17:58:09Z|VERISIGN|ctldbatch|2021-06-30T18:33:09Z|
LMCCADE|52563_CONTACT_GOV-VRSN|919-000-0000||918-000-0000||reginald.ratcliff4@test.com|GSA|VERISIGN|ctldbatch|2021-06-30T17:58:09Z|VERISIGN|ctldbatch|2021-06-30T18:18:09Z|
TESTUSER|12363_CONTACT|123-123-1234||918-000-0000||testuser@gmail.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T17:58:09Z|SOMECOMPANY|ctldbatch|2021-06-30T18:18:09Z|
USER1|12345_CONTACT|123-123-1234||918-000-0000||agustina.wyman7@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-29T18:53:09Z|SOMECOMPANY|ctldbatch|2021-06-29T18:58:08Z|
USER2|12355_CONTACT|123-123-1234||918-000-0000||susy.martin4@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T15:23:10Z|SOMECOMPANY|ctldbatch|2021-06-30T15:38:10Z|
USER3|12356_CONTACT|123-123-1234||918-000-0000||stephania.winters4@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T15:23:10Z|SOMECOMPANY|ctldbatch|2021-06-30T18:28:09Z|
USER4|12357_CONTACT|123-123-1234||918-000-0000||alexandra.bobbitt5@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T15:23:10Z|SOMECOMPANY|ctldbatch|2021-08-02T22:13:09Z|
USER5|12362_CONTACT|123-123-1234||918-000-0000||jospeh.mcdowell3@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T17:58:09Z|SOMECOMPANY|ctldbatch|2021-06-30T18:33:09Z|
USER6|12363_CONTACT|123-123-1234||918-000-0000||reginald.ratcliff4@test.com|GSA|SOMECOMPANY|ctldbatch|2021-06-30T17:58:09Z|SOMECOMPANY|ctldbatch|2021-06-30T18:18:09Z|

View file

@ -1,8 +1,8 @@
Anomaly.gov|ANOMALY|tech
TestDomain.gov|TESTUSER|admin
NEHRP.GOV|RJD1|admin
NEHRP.GOV|JAKING|tech
NEHRP.GOV|JBOONE|billing
NELSONCOUNTY-VA.GOV|MKELLEY|admin
NELSONCOUNTY-VA.GOV|CWILSON|billing
NELSONCOUNTY-VA.GOV|LMCCADE|tech
Anomaly.gov|ANOMALY|tech
TestDomain.gov|TESTUSER|admin
FakeWebsite1|USER1|admin
FakeWebsite1|USER2|tech
FakeWebsite1|USER3|billing
FakeWebsite2.GOV|USER4|admin
FakeWebsite2.GOV|USER5|billing
FakeWebsite2.GOV|USER6|tech

View file

@ -1,4 +1,4 @@
Anomaly.gov|muahaha|
TestDomain.gov|ok|
NEHRP.GOV|serverHold|
NELSONCOUNTY-VA.GOV|Hold|
TestDomain.gov|ok|
FakeWebsite1.GOV|serverHold|
FakeWebsite2.GOV|Hold|

View 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)

View file

@ -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.
@ -1632,7 +1638,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):
@ -1800,7 +1811,11 @@ class TestDomainSecurityEmail(TestDomainOverview):
(
"RegistryError",
form_data_registry_error,
"Update failed. Cannot contact the registry.",
"""
Were 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."),
(
@ -1835,7 +1850,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

View 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)

View file

@ -39,9 +39,11 @@ class GenericError(Exception):
"""
_error_mapping = {
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
"Update failed. Cannot contact the registry."
),
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: """
Were 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."),
}