Skip to content

Commit

Permalink
Merge pull request #1224 from cisagov/dk/996-reports
Browse files Browse the repository at this point in the history
Issue 996 - Basic reports
  • Loading branch information
dave-kennedy-ecs authored Oct 31, 2023
2 parents 5c90303 + 0d504c7 commit 0a6c7ac
Show file tree
Hide file tree
Showing 6 changed files with 436 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +11,7 @@
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
Expand Down Expand Up @@ -747,8 +749,59 @@ def organization_type(self, obj):
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 = {
Expand Down
45 changes: 45 additions & 0 deletions src/registrar/assets/sass/_theme/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 23 additions & 0 deletions src/registrar/templates/django/admin/domain_change_list.html
Original file line number Diff line number Diff line change
@@ -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 changes: 195 additions & 0 deletions src/registrar/tests/test_reports.py
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]"
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)
1 change: 1 addition & 0 deletions src/registrar/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,7 @@ def tearDown(self):
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()
Expand Down
Loading

0 comments on commit 0a6c7ac

Please sign in to comment.