mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-19 17:25:56 +02:00
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:
commit
5afc534370
17 changed files with 618 additions and 78 deletions
|
@ -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:
|
||||
|
|
|
@ -4,7 +4,7 @@ applications:
|
|||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
instances: 2
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
|
|
|
@ -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")
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
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 }}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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|
|
|
@ -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
|
|
@ -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|
|
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.
|
||||
|
@ -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.",
|
||||
"""
|
||||
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."),
|
||||
(
|
||||
|
@ -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
|
||||
|
|
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."),
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue