mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-12 05:58:18 +02:00
Merge branch 'main' into za/additional-data-transferred-domains
This commit is contained in:
commit
3bb7ea623f
6 changed files with 436 additions and 0 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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
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 %}
|
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()
|
||||
|
|
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)
|
Loading…
Add table
Add a link
Reference in a new issue