mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 19:09:22 +02:00
Merge branch 'main' into za/1848-copy-contact-email-to-clipboard
This commit is contained in:
commit
76b05d9540
23 changed files with 1414 additions and 575 deletions
41
.github/workflows/test-deploy.yaml
vendored
41
.github/workflows/test-deploy.yaml
vendored
|
@ -1,41 +0,0 @@
|
||||||
# This workflow is to for testing a change to our deploy structure and will be deleted when testing finishes
|
|
||||||
|
|
||||||
name: Deploy Main
|
|
||||||
run-name: Run deploy for ${{ github.event.inputs.environment }}
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
type: choice
|
|
||||||
description: Which environment should we run deploy for?
|
|
||||||
options:
|
|
||||||
- development
|
|
||||||
- backup
|
|
||||||
- ky
|
|
||||||
- es
|
|
||||||
- nl
|
|
||||||
- rh
|
|
||||||
- za
|
|
||||||
- gd
|
|
||||||
- rb
|
|
||||||
- ko
|
|
||||||
- ab
|
|
||||||
- rjm
|
|
||||||
- dk
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
CF_USERNAME: CF_${{ github.event.inputs.environment }}_USERNAME
|
|
||||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
|
||||||
steps:
|
|
||||||
- name: Deploy to cloud.gov sandbox
|
|
||||||
uses: cloud-gov/cg-cli-tools@main
|
|
||||||
with:
|
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
|
||||||
cf_org: cisa-dotgov
|
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
|
||||||
cf_command: "push -f ops/manifests/manifest-${{ github.event.inputs.environment }}.yaml --strategy rolling"
|
|
|
@ -4,7 +4,7 @@ applications:
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- python_buildpack
|
- python_buildpack
|
||||||
path: ../../src
|
path: ../../src
|
||||||
instances: 2
|
instances: 1
|
||||||
memory: 512M
|
memory: 512M
|
||||||
stack: cflinuxfs4
|
stack: cflinuxfs4
|
||||||
timeout: 180
|
timeout: 180
|
||||||
|
|
|
@ -3,9 +3,9 @@ import logging
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.functions import Concat, Coalesce
|
|
||||||
from django.db.models import Value, CharField, Q
|
from django.db.models import Value, CharField, Q
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.db.models.functions import Concat, Coalesce
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions
|
from django_fsm import get_available_FIELD_transitions
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
|
@ -16,7 +16,6 @@ from django.urls import reverse
|
||||||
from dateutil.relativedelta import relativedelta # type: ignore
|
from dateutil.relativedelta import relativedelta # type: ignore
|
||||||
from epplibwrapper.errors import ErrorCode, RegistryError
|
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
||||||
from registrar.utility import csv_export
|
|
||||||
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
||||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||||
from django.contrib.admin.views.main import ORDER_VAR
|
from django.contrib.admin.views.main import ORDER_VAR
|
||||||
|
@ -1486,7 +1485,6 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
search_help_text = "Search by domain name."
|
search_help_text = "Search by domain name."
|
||||||
change_form_template = "django/admin/domain_change_form.html"
|
change_form_template = "django/admin/domain_change_form.html"
|
||||||
change_list_template = "django/admin/domain_change_list.html"
|
|
||||||
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]
|
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]
|
||||||
|
|
||||||
# Table ordering
|
# Table ordering
|
||||||
|
@ -1533,56 +1531,6 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
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):
|
def response_change(self, request, obj):
|
||||||
# Create dictionary of action functions
|
# Create dictionary of action functions
|
||||||
ACTION_FUNCTIONS = {
|
ACTION_FUNCTIONS = {
|
||||||
|
@ -1714,9 +1662,11 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
"Error deleting this Domain: "
|
(
|
||||||
f"Can't switch from state '{obj.state}' to 'deleted'"
|
"Error deleting this Domain: "
|
||||||
", must be either 'dns_needed' or 'on_hold'",
|
f"Can't switch from state '{obj.state}' to 'deleted'"
|
||||||
|
", must be either 'dns_needed' or 'on_hold'"
|
||||||
|
),
|
||||||
messages.ERROR,
|
messages.ERROR,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -1728,7 +1678,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("Domain %s has been deleted. Thanks!") % obj.name,
|
"Domain %s has been deleted. Thanks!" % obj.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
@ -1770,7 +1720,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("%s is in client hold. This domain is no longer accessible on the public internet.") % obj.name,
|
"%s is in client hold. This domain is no longer accessible on the public internet." % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
@ -1799,7 +1749,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
("%s is ready. This domain is accessible on the public internet.") % obj.name,
|
"%s is ready. This domain is accessible on the public internet." % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
|
|
|
@ -427,43 +427,6 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
|
||||||
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
|
|
||||||
* attach the seleted start and end dates to a url that'll trigger the view, and finally
|
|
||||||
* redirect to that url.
|
|
||||||
*/
|
|
||||||
(function (){
|
|
||||||
|
|
||||||
// Get the current date in the format YYYY-MM-DD
|
|
||||||
let currentDate = new Date().toISOString().split('T')[0];
|
|
||||||
|
|
||||||
// Default the value of the start date input field to the current date
|
|
||||||
let startDateInput =document.getElementById('start');
|
|
||||||
|
|
||||||
// Default the value of the end date input field to the current date
|
|
||||||
let endDateInput =document.getElementById('end');
|
|
||||||
|
|
||||||
let exportGrowthReportButton = document.getElementById('exportLink');
|
|
||||||
|
|
||||||
if (exportGrowthReportButton) {
|
|
||||||
startDateInput.value = currentDate;
|
|
||||||
endDateInput.value = currentDate;
|
|
||||||
|
|
||||||
exportGrowthReportButton.addEventListener('click', function() {
|
|
||||||
// Get the selected start and end dates
|
|
||||||
let startDate = startDateInput.value;
|
|
||||||
let endDate = endDateInput.value;
|
|
||||||
let exportUrl = document.getElementById('exportLink').dataset.exportUrl;
|
|
||||||
|
|
||||||
// Build the URL with parameters
|
|
||||||
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
|
||||||
|
|
||||||
// Redirect to the export URL
|
|
||||||
window.location.href = exportUrl;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
/** An IIFE for admin in DjangoAdmin to listen to changes on the domain request
|
||||||
* status select amd to show/hide the rejection reason
|
* status select amd to show/hide the rejection reason
|
||||||
*/
|
*/
|
||||||
|
|
117
src/registrar/assets/js/get-gov-reports.js
Normal file
117
src/registrar/assets/js/get-gov-reports.js
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
|
||||||
|
* attach the seleted start and end dates to a url that'll trigger the view, and finally
|
||||||
|
* redirect to that url.
|
||||||
|
*
|
||||||
|
* This function also sets the start and end dates to match the url params if they exist
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
// Function to get URL parameter value by name
|
||||||
|
function getParameterByName(name, url) {
|
||||||
|
if (!url) url = window.location.href;
|
||||||
|
name = name.replace(/[\[\]]/g, '\\$&');
|
||||||
|
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||||
|
results = regex.exec(url);
|
||||||
|
if (!results) return null;
|
||||||
|
if (!results[2]) return '';
|
||||||
|
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current date in the format YYYY-MM-DD
|
||||||
|
let currentDate = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Default the value of the start date input field to the current date
|
||||||
|
let startDateInput = document.getElementById('start');
|
||||||
|
|
||||||
|
// Default the value of the end date input field to the current date
|
||||||
|
let endDateInput = document.getElementById('end');
|
||||||
|
|
||||||
|
let exportButtons = document.querySelectorAll('.exportLink');
|
||||||
|
|
||||||
|
if (exportButtons.length > 0) {
|
||||||
|
// Check if start and end dates are present in the URL
|
||||||
|
let urlStartDate = getParameterByName('start_date');
|
||||||
|
let urlEndDate = getParameterByName('end_date');
|
||||||
|
|
||||||
|
// Set input values based on URL parameters or current date
|
||||||
|
startDateInput.value = urlStartDate || currentDate;
|
||||||
|
endDateInput.value = urlEndDate || currentDate;
|
||||||
|
|
||||||
|
exportButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
// Get the selected start and end dates
|
||||||
|
let startDate = startDateInput.value;
|
||||||
|
let endDate = endDateInput.value;
|
||||||
|
let exportUrl = btn.dataset.exportUrl;
|
||||||
|
|
||||||
|
// Build the URL with parameters
|
||||||
|
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
||||||
|
|
||||||
|
// Redirect to the export URL
|
||||||
|
window.location.href = exportUrl;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
createComparativeColumnChart("myChart1", "Managed domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart2", "Unmanaged domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart3", "Deleted domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date");
|
||||||
|
createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date");
|
||||||
|
});
|
||||||
|
|
||||||
|
function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) {
|
||||||
|
var canvas = document.getElementById(canvasId);
|
||||||
|
var ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
var listOne = JSON.parse(canvas.getAttribute('data-list-one'));
|
||||||
|
var listTwo = JSON.parse(canvas.getAttribute('data-list-two'));
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: labelOne,
|
||||||
|
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||||
|
borderColor: "rgba(255, 99, 132, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
data: listOne,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: labelTwo,
|
||||||
|
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||||
|
borderColor: "rgba(75, 192, 192, 1)",
|
||||||
|
borderWidth: 1,
|
||||||
|
data: listTwo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: "bar",
|
||||||
|
data: data,
|
||||||
|
options: options,
|
||||||
|
});
|
||||||
|
}
|
|
@ -112,7 +112,8 @@ html[data-theme="light"] {
|
||||||
.change-list .usa-table thead th,
|
.change-list .usa-table thead th,
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form {
|
body.change-form,
|
||||||
|
.analytics {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -304,7 +305,36 @@ input.admin-confirm-button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.django-admin-modal .usa-prose ul > li {
|
.usa-button-group {
|
||||||
|
margin-left: -0.25rem!important;
|
||||||
|
padding-left: 0!important;
|
||||||
|
.usa-button-group__item {
|
||||||
|
list-style-type: none;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 8px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.usa-icon {
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
a.button:active, a.button:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module--custom {
|
||||||
|
a {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: solid 1px var(--darkened-bg);
|
||||||
|
background: var(--darkened-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-modal--django-admin .usa-prose ul > li {
|
||||||
list-style-type: inherit;
|
list-style-type: inherit;
|
||||||
// Styling based off of the <p> styling in django admin
|
// Styling based off of the <p> styling in django admin
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
|
@ -330,8 +330,9 @@ CSP_FORM_ACTION = allowed_sources
|
||||||
|
|
||||||
# Google analytics requires that we relax our otherwise
|
# Google analytics requires that we relax our otherwise
|
||||||
# strict CSP by allowing scripts to run from their domain
|
# strict CSP by allowing scripts to run from their domain
|
||||||
# and inline with a nonce, as well as allowing connections back to their domain
|
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||||
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/"]
|
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||||
|
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/", "https://cdn.jsdelivr.net/npm/chart.js"]
|
||||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
||||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,16 @@ from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from registrar import views
|
from registrar import views
|
||||||
|
from registrar.views.admin_views import (
|
||||||
from registrar.views.admin_views import ExportData
|
ExportDataDomainsGrowth,
|
||||||
|
ExportDataFederal,
|
||||||
|
ExportDataFull,
|
||||||
|
ExportDataManagedDomains,
|
||||||
|
ExportDataRequestsGrowth,
|
||||||
|
ExportDataType,
|
||||||
|
ExportDataUnmanagedDomains,
|
||||||
|
AnalyticsView,
|
||||||
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step
|
||||||
from registrar.views.utility import always_404
|
from registrar.views.utility import always_404
|
||||||
|
@ -52,7 +59,46 @@ urlpatterns = [
|
||||||
"admin/logout/",
|
"admin/logout/",
|
||||||
RedirectView.as_view(pattern_name="logout", permanent=False),
|
RedirectView.as_view(pattern_name="logout", permanent=False),
|
||||||
),
|
),
|
||||||
path("export_data/", ExportData.as_view(), name="admin_export_data"),
|
path(
|
||||||
|
"admin/analytics/export_data_type/",
|
||||||
|
ExportDataType.as_view(),
|
||||||
|
name="export_data_type",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_data_full/",
|
||||||
|
ExportDataFull.as_view(),
|
||||||
|
name="export_data_full",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_data_federal/",
|
||||||
|
ExportDataFederal.as_view(),
|
||||||
|
name="export_data_federal",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_domains_growth/",
|
||||||
|
ExportDataDomainsGrowth.as_view(),
|
||||||
|
name="export_domains_growth",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_requests_growth/",
|
||||||
|
ExportDataRequestsGrowth.as_view(),
|
||||||
|
name="export_requests_growth",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_managed_domains/",
|
||||||
|
ExportDataManagedDomains.as_view(),
|
||||||
|
name="export_managed_domains",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_unmanaged_domains/",
|
||||||
|
ExportDataUnmanagedDomains.as_view(),
|
||||||
|
name="export_unmanaged_domains",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/",
|
||||||
|
AnalyticsView.as_view(),
|
||||||
|
name="analytics",
|
||||||
|
),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
"domain-request/<id>/edit/",
|
"domain-request/<id>/edit/",
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
<svg width="404" height="409" viewBox="0 0 404 409" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M291.707 328.743C240.024 358.583 133.444 374.87 78.8899 280.379C14.3648 168.618 78.2559 99.3488 140.956 63.1491C203.655 26.9495 296.801 80.4848 337.226 150.503C377.652 220.522 343.391 298.903 291.707 328.743Z" fill="#F5F8FA"/>
|
|
||||||
<circle cx="276.88" cy="130.594" r="8" transform="rotate(135 276.88 130.594)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="288.196" cy="119.279" r="8" transform="rotate(135 288.196 119.279)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.626" cy="175.849" r="8" transform="rotate(135 231.626 175.849)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="186.371" cy="221.104" r="8" transform="rotate(135 186.371 221.104)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="242.939" cy="164.535" r="8" transform="rotate(135 242.939 164.535)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="197.686" cy="209.788" r="8" transform="rotate(135 197.686 209.788)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.312" cy="187.163" r="8" transform="rotate(135 220.312 187.163)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="175.057" cy="232.417" r="8" transform="rotate(135 175.057 232.417)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="163.743" cy="243.731" r="8" transform="rotate(135 163.743 243.731)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="152.43" cy="255.045" r="8" transform="rotate(135 152.43 255.045)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="141.116" cy="266.358" r="8" transform="rotate(135 141.116 266.358)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="129.802" cy="277.672" r="8" transform="rotate(135 129.802 277.672)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="118.489" cy="288.986" r="8" transform="rotate(135 118.489 288.986)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="254.253" cy="153.221" r="8" transform="rotate(135 254.253 153.221)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="265.566" cy="141.908" r="8" transform="rotate(135 265.566 141.908)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="208.998" cy="198.476" r="8" transform="rotate(135 208.998 198.476)" fill="#7AA5C1"/>
|
|
||||||
<circle cx="203.342" cy="203.999" r="120.001" stroke="#7AA5C1" stroke-width="16"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.9 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 53 KiB |
|
@ -1,59 +0,0 @@
|
||||||
<svg width="409" height="214" viewBox="0 0 409 214" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M366.004 90.4612C372.017 135.603 322.608 199.102 196.168 205.902C-32.9139 218.22 19.0655 18.8457 205.511 8.81994C299.204 3.78172 359.99 45.3195 366.004 90.4612Z" fill="#F5F8FA"/>
|
|
||||||
<circle cx="213.873" cy="37.4943" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="212.214" cy="58.4272" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.089" cy="66.2297" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="235.451" cy="85.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="252.535" cy="99.7787" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="273.981" cy="115.121" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="372.072" cy="182.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="402.423" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.441" cy="99.6379" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="231.089" cy="118.336" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="262.722" cy="143.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="248.722" cy="125.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="288.537" cy="138.04" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="300.051" cy="160.304" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="338.722" cy="170.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="349.914" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="330.21" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="316.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="305.722" cy="190.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="287.722" cy="184.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="269.894" cy="183.007" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="250.19" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="220.722" cy="192.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="233.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="252.722" cy="165.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="180.203" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="161.329" cy="196.143" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="140.795" cy="189.575" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<path d="M122.733 189.575C122.733 193.203 119.793 196.143 116.165 196.143C112.538 196.143 109.597 193.203 109.597 189.575C109.597 185.948 112.538 183.007 116.165 183.007C119.793 183.007 122.733 185.948 122.733 189.575Z" fill="#7AA5C1"/>
|
|
||||||
<circle cx="91.5343" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="57.8852" cy="185.432" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="198.722" cy="195.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="193.34" cy="70.2917" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="98.9323" cy="156.735" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="25.247" cy="189.642" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="7.26164" cy="190.584" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="76.7592" cy="173.44" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="129.722" cy="172.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="147.722" cy="164.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="164.722" cy="173.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="174.465" cy="150.167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="186.771" cy="169.871" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="198.266" cy="150.167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="232.747" cy="151.176" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="273.722" cy="162.568" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="210.589" cy="163.303" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="204.834" cy="124.904" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="193.34" cy="108.553" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="171.181" cy="119.345" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="160.499" cy="138.04" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="139.153" cy="144.608" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="170.351" cy="95.4167" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="151.477" cy="115.121" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="125.204" cy="137.031" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="112.881" cy="163.303" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
<circle cx="204.834" cy="86.6427" r="6.56803" fill="#7AA5C1"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.9 KiB |
196
src/registrar/templates/admin/analytics.html
Normal file
196
src/registrar/templates/admin/analytics.html
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="content-main" class="analytics">
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2">
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>At a glance</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
<ul>
|
||||||
|
<li>User Count: {{ data.user_count }}</li>
|
||||||
|
<li>Domain Count: {{ data.domain_count }}</li>
|
||||||
|
<li>Domains in READY state: {{ data.ready_domain_count }}</li>
|
||||||
|
<li>Domain applications (last 30 days): {{ data.last_30_days_applications }}</li>
|
||||||
|
<li>Approved applications (last 30 days): {{ data.last_30_days_approved_applications }}</li>
|
||||||
|
<li>Average approval time for applications (last 30 days): {{ data.average_application_approval_time_last_30_days }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>Current domains</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_type' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">All domain metadata</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_full' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Current full</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_federal' %}" class="button" role="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Current federal</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-top-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<div class="module">
|
||||||
|
<h2>Growth reports</h2>
|
||||||
|
<div class="padding-2">
|
||||||
|
{% comment %}
|
||||||
|
Inputs of type date suck for accessibility.
|
||||||
|
We'll need to replace those guys with a django form once we figure out how to hook one onto this page.
|
||||||
|
See the commit "Review for ticket #999"
|
||||||
|
{% endcomment %}
|
||||||
|
<div class="display-flex flex-align-baseline margin-top-1 margin-bottom-2">
|
||||||
|
<div class="margin-right-1">
|
||||||
|
<label for="start">Start date:</label>
|
||||||
|
<input type="date" id="start" name="start" value="2018-07-22" min="2018-01-01" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="end">End date:</label>
|
||||||
|
<input type="date" id="end" name="end" value="2023-12-01" min="2023-12-01" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Domain growth</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Request growth</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Managed domains</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
|
</svg><span class="margin-left-05">Unmanaged domains</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button class="button exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#assessment"></use>
|
||||||
|
</svg><span class="margin-left-05">Update charts</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart1" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.managed_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.managed_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Managed domains</h2>
|
||||||
|
<p>{{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart2" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.unmanaged_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.unmanaged_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Unmanaged domains</h2>
|
||||||
|
<p>{{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart3" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.deleted_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.deleted_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Deleted domains</h2>
|
||||||
|
<p>{{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart4" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.ready_domains_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.ready_domains_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Ready domains</h2>
|
||||||
|
<p>{{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2 margin-y-2">
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart5" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.submitted_requests_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.submitted_requests_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: Submitted requests</h2>
|
||||||
|
<p>{{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
<div class="grid-col">
|
||||||
|
<canvas id="myChart6" width="400" height="200"
|
||||||
|
aria-label="Chart: {{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}"
|
||||||
|
role="img"
|
||||||
|
data-list-one="{{data.requests_sliced_at_start_date}}"
|
||||||
|
data-list-two="{{data.requests_sliced_at_end_date}}"
|
||||||
|
>
|
||||||
|
<h2>Chart: All requests</h2>
|
||||||
|
<p>{{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}</p>
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -64,6 +64,11 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<div class="module module--custom">
|
||||||
|
<h2>Analytics</h2>
|
||||||
|
<a class="display-block padding-y-1 padding-x-1" href="{% url 'analytics' %}">Dashboard</a>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
>
|
>
|
||||||
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds-init.min.js' %}" defer></script>
|
||||||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
|
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||||
|
|
|
@ -22,7 +22,7 @@ Load our custom filters to extract info from the django generated markup.
|
||||||
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<span>
|
<span>
|
||||||
<input type="checkbox" name="_selected_action" id="action-toggle">
|
<input type="checkbox" id="action-toggle">
|
||||||
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
{% extends "admin/index.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div id="content-main">
|
|
||||||
{% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}
|
|
||||||
<div class="custom-content module">
|
|
||||||
<h2>Reports</h2>
|
|
||||||
<h3>Domain growth report</h3>
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
Inputs of type date suck for accessibility.
|
|
||||||
We'll need to replace those guys with a django form once we figure out how to hook one onto this page.
|
|
||||||
The challenge is in the path definition in urls. Itdoes NOT like admin/export_data/
|
|
||||||
|
|
||||||
See the commit "Review for ticket #999"
|
|
||||||
{% endcomment %}
|
|
||||||
|
|
||||||
<div class="display-flex flex-align-baseline flex-justify margin-y-1">
|
|
||||||
<div>
|
|
||||||
<label for="start">Start date:</label>
|
|
||||||
<input type="date" id="start" name="start" value="2018-07-22" min="2018-01-01" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="end">End date:</label>
|
|
||||||
<input type="date" id="end" name="end" value="2023-12-01" min="2023-12-01" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="exportLink" data-export-url="{% url 'admin_export_data' %}" type="button" class="button">Export</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
|
@ -54,7 +54,7 @@
|
||||||
|
|
||||||
{# Create a modal for the _extend_expiration_date button #}
|
{# Create a modal for the _extend_expiration_date button #}
|
||||||
<div
|
<div
|
||||||
class="usa-modal django-admin-modal"
|
class="usa-modal usa-modal--django-admin"
|
||||||
id="toggle-extend-expiration-alert"
|
id="toggle-extend-expiration-alert"
|
||||||
aria-labelledby="Are you sure you want to extend the expiration date?"
|
aria-labelledby="Are you sure you want to extend the expiration date?"
|
||||||
aria-describedby="This expiration date will be extended."
|
aria-describedby="This expiration date will be extended."
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
|
|
||||||
{# Create a modal for the _on_hold button #}
|
{# Create a modal for the _on_hold button #}
|
||||||
<div
|
<div
|
||||||
class="usa-modal django-admin-modal"
|
class="usa-modal usa-modal--django-admin"
|
||||||
id="toggle-place-on-hold"
|
id="toggle-place-on-hold"
|
||||||
aria-labelledby="Are you sure you want to place this domain on hold?"
|
aria-labelledby="Are you sure you want to place this domain on hold?"
|
||||||
aria-describedby="This domain will be put on hold"
|
aria-describedby="This domain will be put on hold"
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
</div>
|
</div>
|
||||||
{# Create a modal for the _remove_domain button #}
|
{# Create a modal for the _remove_domain button #}
|
||||||
<div
|
<div
|
||||||
class="usa-modal django-admin-modal"
|
class="usa-modal usa-modal--django-admin"
|
||||||
id="toggle-remove-from-registry"
|
id="toggle-remove-from-registry"
|
||||||
aria-labelledby="Are you sure you want to remove this domain from the registry?"
|
aria-labelledby="Are you sure you want to remove this domain from the registry?"
|
||||||
aria-describedby="This domain will be removed."
|
aria-describedby="This domain will be removed."
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,4 +1,3 @@
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -13,6 +12,8 @@ from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model, login
|
from django.contrib.auth import get_user_model, login
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
|
@ -35,6 +36,7 @@ from epplibwrapper import (
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
responses,
|
responses,
|
||||||
)
|
)
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||||
|
|
||||||
|
@ -492,6 +494,184 @@ class AuditedAdminMockData:
|
||||||
return domain_request
|
return domain_request
|
||||||
|
|
||||||
|
|
||||||
|
class MockDb(TestCase):
|
||||||
|
"""Hardcoded mocks make test case assertions straightforward."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a time-aware current date
|
||||||
|
current_datetime = timezone.now()
|
||||||
|
# Extract the date part
|
||||||
|
current_date = current_datetime.date()
|
||||||
|
# Create start and end dates using timedelta
|
||||||
|
self.end_date = current_date + timedelta(days=2)
|
||||||
|
self.start_date = current_date - timedelta(days=2)
|
||||||
|
|
||||||
|
self.domain_1, _ = Domain.objects.get_or_create(
|
||||||
|
name="cdomain1.gov", state=Domain.State.READY, first_ready=timezone.now()
|
||||||
|
)
|
||||||
|
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_5, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
|
||||||
|
)
|
||||||
|
self.domain_6, _ = Domain.objects.get_or_create(
|
||||||
|
name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
|
||||||
|
)
|
||||||
|
self.domain_7, _ = Domain.objects.get_or_create(
|
||||||
|
name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
||||||
|
)
|
||||||
|
self.domain_8, _ = Domain.objects.get_or_create(
|
||||||
|
name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
||||||
|
)
|
||||||
|
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
||||||
|
# and a specific time (using datetime.min.time()).
|
||||||
|
# Deleted yesterday
|
||||||
|
self.domain_9, _ = Domain.objects.get_or_create(
|
||||||
|
name="zdomain9.gov",
|
||||||
|
state=Domain.State.DELETED,
|
||||||
|
deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
|
||||||
|
)
|
||||||
|
# ready tomorrow
|
||||||
|
self.domain_10, _ = Domain.objects.get_or_create(
|
||||||
|
name="adomain10.gov",
|
||||||
|
state=Domain.State.READY,
|
||||||
|
first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
self.domain_information_2, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user, domain=self.domain_2, organization_type="interstate", is_election_board=True
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
is_election_board=True,
|
||||||
|
)
|
||||||
|
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_5,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_6, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_6,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_7, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_7,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_8, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_8,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_9, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_9,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
self.domain_information_10, _ = DomainInformation.objects.get_or_create(
|
||||||
|
creator=self.user,
|
||||||
|
domain=self.domain_10,
|
||||||
|
organization_type="federal",
|
||||||
|
federal_agency="Armed Forces Retirement Home",
|
||||||
|
is_election_board=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
meoward_user = get_user_model().objects.create(
|
||||||
|
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
lebowski_user = get_user_model().objects.create(
|
||||||
|
username="big_lebowski", first_name="big", last_name="lebowski", email="big_lebowski@dude.co"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=lebowski_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
_, created = UserDomainRole.objects.get_or_create(
|
||||||
|
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
self.domain_request_1 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_2 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW, name="city2.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_3 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city3.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_4 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED, name="city4.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_5 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.APPROVED, name="city5.gov"
|
||||||
|
)
|
||||||
|
self.domain_request_3.submit()
|
||||||
|
self.domain_request_3.save()
|
||||||
|
self.domain_request_4.submit()
|
||||||
|
self.domain_request_4.save()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
def mock_user():
|
def mock_user():
|
||||||
"""A simple user."""
|
"""A simple user."""
|
||||||
user_kwargs = dict(
|
user_kwargs = dict(
|
||||||
|
@ -680,7 +860,7 @@ class MockEppLib(TestCase):
|
||||||
self,
|
self,
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
pw="thisisnotapassword",
|
pw="thisisnotapassword",
|
||||||
):
|
):
|
||||||
fake = info.InfoContactResultData(
|
fake = info.InfoContactResultData(
|
||||||
|
@ -718,82 +898,82 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
mockDataInfoDomain = fakedEppObject(
|
mockDataInfoDomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomain = fakedEppObject(
|
mockDataInfoDomainSubdomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meoward.gov"],
|
hosts=["fake.meoward.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
mockDataInfoDomainSubdomainAndIPAddress = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meow.gov"],
|
hosts=["fake.meow.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
addrs=[common.Ip(addr="2.0.0.8")],
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
mockDataInfoDomainNotSubdomainNoIP = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.meow.com"],
|
hosts=["fake.meow.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
mockDataInfoDomainSubdomainNoIP = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.subdomainwoip.gov"],
|
hosts=["fake.subdomainwoip.gov"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataExtensionDomain = fakedEppObject(
|
mockDataExtensionDomain = fakedEppObject(
|
||||||
"fakePw",
|
"fakePw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
statuses=[
|
statuses=[
|
||||||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||||
common.Status(state="inactive", description="", lang="en"),
|
common.Status(state="inactive", description="", lang="en"),
|
||||||
],
|
],
|
||||||
ex_date=datetime.date(2023, 11, 15),
|
ex_date=date(2023, 11, 15),
|
||||||
)
|
)
|
||||||
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
||||||
"123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
"123", "123@mail.gov", datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
||||||
)
|
)
|
||||||
InfoDomainWithContacts = fakedEppObject(
|
InfoDomainWithContacts = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -818,7 +998,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
InfoDomainWithDefaultSecurityContact = fakedEppObject(
|
InfoDomainWithDefaultSecurityContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultSec",
|
contact="defaultSec",
|
||||||
|
@ -833,11 +1013,11 @@ class MockEppLib(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
mockVerisignDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
||||||
"defaultVeri", "registrar@dotgov.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
"defaultVeri", "registrar@dotgov.gov", datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
||||||
)
|
)
|
||||||
InfoDomainWithVerisignSecurityContact = fakedEppObject(
|
InfoDomainWithVerisignSecurityContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultVeri",
|
contact="defaultVeri",
|
||||||
|
@ -853,7 +1033,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
|
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
|
||||||
"fakepw",
|
"fakepw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="defaultTech",
|
contact="defaultTech",
|
||||||
|
@ -878,14 +1058,14 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainNoContact = fakedEppObject(
|
infoDomainNoContact = fakedEppObject(
|
||||||
"security",
|
"security",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=["fake.host.com"],
|
hosts=["fake.host.com"],
|
||||||
)
|
)
|
||||||
|
|
||||||
infoDomainThreeHosts = fakedEppObject(
|
infoDomainThreeHosts = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[
|
hosts=[
|
||||||
"ns1.my-nameserver-1.com",
|
"ns1.my-nameserver-1.com",
|
||||||
|
@ -896,43 +1076,43 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainNoHost = fakedEppObject(
|
infoDomainNoHost = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[],
|
hosts=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
infoDomainTwoHosts = fakedEppObject(
|
infoDomainTwoHosts = fakedEppObject(
|
||||||
"my-nameserver.gov",
|
"my-nameserver.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
|
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHosts = fakedEppObject(
|
mockDataInfoHosts = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)),
|
||||||
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHosts1IP = fakedEppObject(
|
mockDataInfoHosts1IP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)),
|
||||||
addrs=[common.Ip(addr="2.0.0.8")],
|
addrs=[common.Ip(addr="2.0.0.8")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
mockDataInfoHostsNotSubdomainNoIP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 26, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 26, 19, 45, 35)),
|
||||||
addrs=[],
|
addrs=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
mockDataInfoHostsSubdomainNoIP = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 8, 27, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 8, 27, 19, 45, 35)),
|
||||||
addrs=[],
|
addrs=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime(2023, 8, 25, 19, 45, 35)))
|
||||||
addDsData1 = {
|
addDsData1 = {
|
||||||
"keyTag": 1234,
|
"keyTag": 1234,
|
||||||
"alg": 3,
|
"alg": 3,
|
||||||
|
@ -964,7 +1144,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainHasIP = fakedEppObject(
|
infoDomainHasIP = fakedEppObject(
|
||||||
"nameserverwithip.gov",
|
"nameserverwithip.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -989,7 +1169,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
justNameserver = fakedEppObject(
|
justNameserver = fakedEppObject(
|
||||||
"justnameserver.com",
|
"justnameserver.com",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[
|
contacts=[
|
||||||
common.DomainContact(
|
common.DomainContact(
|
||||||
contact="securityContact",
|
contact="securityContact",
|
||||||
|
@ -1012,7 +1192,7 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
infoDomainCheckHostIPCombo = fakedEppObject(
|
infoDomainCheckHostIPCombo = fakedEppObject(
|
||||||
"nameserversubdomain.gov",
|
"nameserversubdomain.gov",
|
||||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
cr_date=make_aware(datetime(2023, 5, 25, 19, 45, 35)),
|
||||||
contacts=[],
|
contacts=[],
|
||||||
hosts=[
|
hosts=[
|
||||||
"ns1.nameserversubdomain.gov",
|
"ns1.nameserversubdomain.gov",
|
||||||
|
@ -1022,27 +1202,27 @@ class MockEppLib(TestCase):
|
||||||
|
|
||||||
mockRenewedDomainExpDate = fakedEppObject(
|
mockRenewedDomainExpDate = fakedEppObject(
|
||||||
"fake.gov",
|
"fake.gov",
|
||||||
ex_date=datetime.date(2023, 5, 25),
|
ex_date=date(2023, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockButtonRenewedDomainExpDate = fakedEppObject(
|
mockButtonRenewedDomainExpDate = fakedEppObject(
|
||||||
"fake.gov",
|
"fake.gov",
|
||||||
ex_date=datetime.date(2025, 5, 25),
|
ex_date=date(2025, 5, 25),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
mockDnsNeededRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakeneeded.gov",
|
"fakeneeded.gov",
|
||||||
ex_date=datetime.date(2023, 2, 15),
|
ex_date=date(2023, 2, 15),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockMaximumRenewedDomainExpDate = fakedEppObject(
|
mockMaximumRenewedDomainExpDate = fakedEppObject(
|
||||||
"fakemaximum.gov",
|
"fakemaximum.gov",
|
||||||
ex_date=datetime.date(2024, 12, 31),
|
ex_date=date(2024, 12, 31),
|
||||||
)
|
)
|
||||||
|
|
||||||
mockRecentRenewedDomainExpDate = fakedEppObject(
|
mockRecentRenewedDomainExpDate = fakedEppObject(
|
||||||
"waterbutpurple.gov",
|
"waterbutpurple.gov",
|
||||||
ex_date=datetime.date(2024, 11, 15),
|
ex_date=date(2024, 11, 15),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _mockDomainName(self, _name, _avail=False):
|
def _mockDomainName(self, _name, _avail=False):
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.urls import reverse
|
||||||
from registrar.tests.common import create_superuser
|
from registrar.tests.common import create_superuser
|
||||||
|
|
||||||
|
|
||||||
class TestViews(TestCase):
|
class TestAdminViews(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.superuser = create_superuser()
|
self.superuser = create_superuser()
|
||||||
|
@ -26,7 +26,7 @@ class TestViews(TestCase):
|
||||||
|
|
||||||
# Construct the URL for the export data view with start_date and end_date parameters:
|
# Construct the URL for the export data view with start_date and end_date parameters:
|
||||||
# This stuff is currently done in JS
|
# This stuff is currently done in JS
|
||||||
export_data_url = reverse("admin_export_data") + f"?start_date={start_date}&end_date={end_date}"
|
export_data_url = reverse("export_domains_growth") + f"?start_date={start_date}&end_date={end_date}"
|
||||||
|
|
||||||
# Make a GET request to the export data page
|
# Make a GET request to the export data page
|
||||||
response = self.client.get(export_data_url)
|
response = self.client.get(export_data_url)
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from django.test import Client, RequestFactory, TestCase
|
from django.test import Client, RequestFactory
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from registrar.models.domain_information import DomainInformation
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.public_contact import PublicContact
|
|
||||||
from registrar.models.user import User
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
|
||||||
from registrar.tests.common import MockEppLib
|
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
write_csv,
|
export_data_managed_domains_to_csv,
|
||||||
|
export_data_unmanaged_domains_to_csv,
|
||||||
|
get_sliced_domains,
|
||||||
|
get_sliced_requests,
|
||||||
|
write_domains_csv,
|
||||||
get_default_start_date,
|
get_default_start_date,
|
||||||
get_default_end_date,
|
get_default_end_date,
|
||||||
|
write_requests_csv,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
@ -22,62 +22,19 @@ from django.conf import settings
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .common import less_console_noise
|
from .common import MockDb, MockEppLib, less_console_noise
|
||||||
|
|
||||||
|
|
||||||
class CsvReportsTest(TestCase):
|
class CsvReportsTest(MockDb):
|
||||||
"""Tests to determine if we are uploading our reports correctly"""
|
"""Tests to determine if we are uploading our reports correctly"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Create fake domain data"""
|
"""Create fake domain data"""
|
||||||
|
super().setUp()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
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_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):
|
|
||||||
"""Delete all faked data"""
|
|
||||||
Domain.objects.all().delete()
|
|
||||||
DomainInformation.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
|
||||||
super().tearDown()
|
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_generate_federal_report(self):
|
def test_generate_federal_report(self):
|
||||||
|
@ -88,6 +45,7 @@ class CsvReportsTest(TestCase):
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||||
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
]
|
]
|
||||||
# We don't actually want to write anything for a test case,
|
# We don't actually want to write anything for a test case,
|
||||||
|
@ -108,6 +66,7 @@ class CsvReportsTest(TestCase):
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||||
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||||
]
|
]
|
||||||
|
@ -166,6 +125,7 @@ class CsvReportsTest(TestCase):
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_load_federal_report(self):
|
def test_load_federal_report(self):
|
||||||
"""Tests the get_current_federal api endpoint"""
|
"""Tests the get_current_federal api endpoint"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_instance = mock_client.return_value
|
mock_client_instance = mock_client.return_value
|
||||||
|
@ -199,6 +159,7 @@ class CsvReportsTest(TestCase):
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_load_full_report(self):
|
def test_load_full_report(self):
|
||||||
"""Tests the current-federal api link"""
|
"""Tests the current-federal api link"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_instance = mock_client.return_value
|
mock_client_instance = mock_client.return_value
|
||||||
|
@ -231,141 +192,17 @@ class CsvReportsTest(TestCase):
|
||||||
self.assertEqual(expected_file_content, response.content)
|
self.assertEqual(expected_file_content, response.content)
|
||||||
|
|
||||||
|
|
||||||
class ExportDataTest(MockEppLib):
|
class ExportDataTest(MockDb, MockEppLib):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
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, first_ready=timezone.now()
|
|
||||||
)
|
|
||||||
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_5, _ = Domain.objects.get_or_create(
|
|
||||||
name="bdomain5.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(2023, 11, 1))
|
|
||||||
)
|
|
||||||
self.domain_6, _ = Domain.objects.get_or_create(
|
|
||||||
name="bdomain6.gov", state=Domain.State.DELETED, deleted=timezone.make_aware(datetime(1980, 10, 16))
|
|
||||||
)
|
|
||||||
self.domain_7, _ = Domain.objects.get_or_create(
|
|
||||||
name="xdomain7.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
|
||||||
)
|
|
||||||
self.domain_8, _ = Domain.objects.get_or_create(
|
|
||||||
name="sdomain8.gov", state=Domain.State.DELETED, deleted=timezone.now()
|
|
||||||
)
|
|
||||||
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
|
||||||
# and a specific time (using datetime.min.time()).
|
|
||||||
# Deleted yesterday
|
|
||||||
self.domain_9, _ = Domain.objects.get_or_create(
|
|
||||||
name="zdomain9.gov",
|
|
||||||
state=Domain.State.DELETED,
|
|
||||||
deleted=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
|
|
||||||
)
|
|
||||||
# ready tomorrow
|
|
||||||
self.domain_10, _ = Domain.objects.get_or_create(
|
|
||||||
name="adomain10.gov",
|
|
||||||
state=Domain.State.READY,
|
|
||||||
first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
|
|
||||||
)
|
|
||||||
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
self.domain_information_5, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_5,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_6, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_6,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_7, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_7,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_8, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_8,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_9, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_9,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
self.domain_information_10, _ = DomainInformation.objects.get_or_create(
|
|
||||||
creator=self.user,
|
|
||||||
domain=self.domain_10,
|
|
||||||
organization_type="federal",
|
|
||||||
federal_agency="Armed Forces Retirement Home",
|
|
||||||
)
|
|
||||||
|
|
||||||
meoward_user = get_user_model().objects.create(
|
|
||||||
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test for more than 1 domain manager
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test for just 1 domain manager
|
|
||||||
_, created = UserDomainRole.objects.get_or_create(
|
|
||||||
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
|
|
||||||
)
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
PublicContact.objects.all().delete()
|
|
||||||
Domain.objects.all().delete()
|
|
||||||
DomainInformation.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
|
||||||
UserDomainRole.objects.all().delete()
|
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
def test_export_domains_to_writer_security_emails(self):
|
def test_export_domains_to_writer_security_emails(self):
|
||||||
"""Test that export_domains_to_writer returns the
|
"""Test that export_domains_to_writer returns the
|
||||||
expected security email"""
|
expected security email"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Add security email information
|
# Add security email information
|
||||||
self.domain_1.name = "defaultsecurity.gov"
|
self.domain_1.name = "defaultsecurity.gov"
|
||||||
|
@ -403,7 +240,7 @@ class ExportDataTest(MockEppLib):
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -427,10 +264,11 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_csv(self):
|
def test_write_domains_csv(self):
|
||||||
"""Test that write_body returns the
|
"""Test that write_body returns the
|
||||||
existing domain, test that sort by domain name works,
|
existing domain, test that sort by domain name works,
|
||||||
test that filter works"""
|
test that filter works"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
|
@ -462,7 +300,7 @@ class ExportDataTest(MockEppLib):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
|
@ -486,8 +324,9 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_body_additional(self):
|
def test_write_domains_body_additional(self):
|
||||||
"""An additional test for filters and multi-column sort"""
|
"""An additional test for filters and multi-column sort"""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
|
@ -512,7 +351,7 @@ class ExportDataTest(MockEppLib):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
)
|
)
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
|
@ -535,27 +374,23 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
def test_write_body_with_date_filter_pulls_domains_in_range(self):
|
def test_write_domains_body_with_date_filter_pulls_domains_in_range(self):
|
||||||
"""Test that domains that are
|
"""Test that domains that are
|
||||||
1. READY and their first_ready dates are in range
|
1. READY and their first_ready dates are in range
|
||||||
2. DELETED and their deleted dates are in range
|
2. DELETED and their deleted dates are in range
|
||||||
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
||||||
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
||||||
|
|
||||||
We considered testing export_data_growth_to_csv which calls write_body
|
We considered testing export_data_domain_growth_to_csv which calls write_body
|
||||||
and would have been easy to set up, but expected_content would contain created_at dates
|
and would have been easy to set up, but expected_content would contain created_at dates
|
||||||
which are hard to mock.
|
which are hard to mock.
|
||||||
|
|
||||||
TODO: Simplify is created_at is not needed for the report."""
|
TODO: Simplify if created_at is not needed for the report."""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
# We use timezone.make_aware to sync to server time a datetime object with the current date
|
|
||||||
# (using date.today()) and a specific time (using datetime.min.time()).
|
|
||||||
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time()))
|
|
||||||
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
|
|
||||||
|
|
||||||
# Define columns, sort fields, and filter condition
|
# Define columns, sort fields, and filter condition
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
|
@ -579,19 +414,19 @@ class ExportDataTest(MockEppLib):
|
||||||
"domain__state__in": [
|
"domain__state__in": [
|
||||||
Domain.State.READY,
|
Domain.State.READY,
|
||||||
],
|
],
|
||||||
"domain__first_ready__lte": end_date,
|
"domain__first_ready__lte": self.end_date,
|
||||||
"domain__first_ready__gte": start_date,
|
"domain__first_ready__gte": self.start_date,
|
||||||
}
|
}
|
||||||
filter_conditions_for_deleted_domains = {
|
filter_conditions_for_deleted_domains = {
|
||||||
"domain__state__in": [
|
"domain__state__in": [
|
||||||
Domain.State.DELETED,
|
Domain.State.DELETED,
|
||||||
],
|
],
|
||||||
"domain__deleted__lte": end_date,
|
"domain__deleted__lte": self.end_date,
|
||||||
"domain__deleted__gte": start_date,
|
"domain__deleted__gte": self.start_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields,
|
sort_fields,
|
||||||
|
@ -599,7 +434,7 @@ class ExportDataTest(MockEppLib):
|
||||||
get_domain_managers=False,
|
get_domain_managers=False,
|
||||||
should_write_header=True,
|
should_write_header=True,
|
||||||
)
|
)
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields_for_deleted_domains,
|
sort_fields_for_deleted_domains,
|
||||||
|
@ -634,13 +469,13 @@ class ExportDataTest(MockEppLib):
|
||||||
|
|
||||||
def test_export_domains_to_writer_domain_managers(self):
|
def test_export_domains_to_writer_domain_managers(self):
|
||||||
"""Test that export_domains_to_writer returns the
|
"""Test that export_domains_to_writer returns the
|
||||||
expected domain managers"""
|
expected domain managers."""
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
csv_file = StringIO()
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
# Define columns, sort fields, and filter condition
|
# Define columns, sort fields, and filter condition
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
"Status",
|
"Status",
|
||||||
|
@ -664,7 +499,7 @@ class ExportDataTest(MockEppLib):
|
||||||
}
|
}
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv(
|
write_domains_csv(
|
||||||
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -677,11 +512,11 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Status,Expiration date,Domain type,Agency,"
|
"Domain name,Status,Expiration date,Domain type,Agency,"
|
||||||
"Organization name,City,State,AO,AO email,"
|
"Organization name,City,State,AO,AO email,"
|
||||||
"Security contact email,Domain manager email 1,Domain manager email 2,\n"
|
"Security contact email,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||||
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
|
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
|
||||||
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
|
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
|
||||||
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
|
||||||
", , , ,meoward@rocks.com,info@example.com\n"
|
", , , ,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n"
|
||||||
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
||||||
)
|
)
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
@ -690,8 +525,132 @@ class ExportDataTest(MockEppLib):
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
|
def test_export_data_managed_domains_to_csv(self):
|
||||||
|
"""Test get counts for domains that have domain managers for two different dates,
|
||||||
|
get list of managed domains at end_date."""
|
||||||
|
|
||||||
class HelperFunctions(TestCase):
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
export_data_managed_domains_to_csv(
|
||||||
|
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
self.maxDiff = None
|
||||||
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||||
|
"Special district,School district,Election office\n"
|
||||||
|
"1,1,0,0,0,0,0,0,0,1\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n"
|
||||||
|
"cdomain1.gov,Federal - Executive,meoward@rocks.com,info@example.com,big_lebowski@dude.co\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_data_unmanaged_domains_to_csv(self):
|
||||||
|
"""Test get counts for domains that do not have domain managers for two different dates,
|
||||||
|
get list of unmanaged domains at end_date."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
export_data_unmanaged_domains_to_csv(
|
||||||
|
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
self.maxDiff = None
|
||||||
|
# We expect the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"UNMANAGED DOMAINS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"UNMANAGED DOMAINS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"1,1,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type\n"
|
||||||
|
"adomain10.gov,Federal\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_write_requests_body_with_date_filter_pulls_requests_in_range(self):
|
||||||
|
"""Test that requests that are
|
||||||
|
1. SUBMITTED and their submission_date are in range
|
||||||
|
are pulled when the growth report conditions are applied to export_requests_to_writed.
|
||||||
|
Test that requests are sorted by requested domain name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# Define columns, sort fields, and filter condition
|
||||||
|
# We'll skip submission date because it's dynamic and therefore
|
||||||
|
# impossible to set in expected_content
|
||||||
|
columns = [
|
||||||
|
"Requested domain",
|
||||||
|
"Organization type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"requested_domain__name",
|
||||||
|
]
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": self.end_date,
|
||||||
|
"submission_date__gte": self.start_date,
|
||||||
|
}
|
||||||
|
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||||
|
# 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 first, created between today-2 and today+2, sorted by created_at then name
|
||||||
|
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||||
|
expected_content = (
|
||||||
|
"Requested domain,Organization type\n"
|
||||||
|
"city3.gov,Federal - Executive\n"
|
||||||
|
"city4.gov,Federal - Executive\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)
|
||||||
|
|
||||||
|
|
||||||
|
class HelperFunctions(MockDb):
|
||||||
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
|
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
|
||||||
|
|
||||||
def test_get_default_start_date(self):
|
def test_get_default_start_date(self):
|
||||||
|
@ -704,3 +663,33 @@ class HelperFunctions(TestCase):
|
||||||
expected_date = timezone.now()
|
expected_date = timezone.now()
|
||||||
actual_date = get_default_end_date()
|
actual_date = get_default_end_date()
|
||||||
self.assertEqual(actual_date.date(), expected_date.date())
|
self.assertEqual(actual_date.date(), expected_date.date())
|
||||||
|
|
||||||
|
def test_get_sliced_domains(self):
|
||||||
|
"""Should get fitered domains counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
filter_condition = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": self.end_date,
|
||||||
|
}
|
||||||
|
# Test with distinct
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True)
|
||||||
|
expected_content = [1, 1, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
|
# Test without distinct
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
||||||
|
expected_content = [1, 3, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||||
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
|
def test_get_sliced_requests(self):
|
||||||
|
"""Should get fitered requests counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": self.end_date,
|
||||||
|
}
|
||||||
|
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
||||||
|
expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
from collections import Counter
|
||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain_information import DomainInformation
|
from registrar.models.domain_information import DomainInformation
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
@ -19,16 +21,22 @@ def write_header(writer, columns):
|
||||||
Receives params from the parent methods and outputs a CSV with a header row.
|
Receives params from the parent methods and outputs a CSV with a header row.
|
||||||
Works with write_header as long as the same writer object is passed.
|
Works with write_header as long as the same writer object is passed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
writer.writerow(columns)
|
writer.writerow(columns)
|
||||||
|
|
||||||
|
|
||||||
def get_domain_infos(filter_condition, sort_fields):
|
def get_domain_infos(filter_condition, sort_fields):
|
||||||
|
"""
|
||||||
|
Returns DomainInformation objects filtered and sorted based on the provided conditions.
|
||||||
|
filter_condition -> A dictionary of conditions to filter the objects.
|
||||||
|
sort_fields -> A list of fields to sort the resulting query set.
|
||||||
|
returns: A queryset of DomainInformation objects
|
||||||
|
"""
|
||||||
domain_infos = (
|
domain_infos = (
|
||||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||||
.prefetch_related("domain__permissions")
|
.prefetch_related("domain__permissions")
|
||||||
.filter(**filter_condition)
|
.filter(**filter_condition)
|
||||||
.order_by(*sort_fields)
|
.order_by(*sort_fields)
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Do a mass concat of the first and last name fields for authorizing_official.
|
# Do a mass concat of the first and last name fields for authorizing_official.
|
||||||
|
@ -45,7 +53,7 @@ def get_domain_infos(filter_condition, sort_fields):
|
||||||
return domain_infos_cleaned
|
return domain_infos_cleaned
|
||||||
|
|
||||||
|
|
||||||
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
|
def parse_domain_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
|
||||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||||
|
|
||||||
# Domain should never be none when parsing this information
|
# Domain should never be none when parsing this information
|
||||||
|
@ -129,7 +137,7 @@ def _get_security_emails(sec_contact_ids):
|
||||||
return security_emails_dict
|
return security_emails_dict
|
||||||
|
|
||||||
|
|
||||||
def write_csv(
|
def write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields,
|
sort_fields,
|
||||||
|
@ -138,10 +146,10 @@ def write_csv(
|
||||||
should_write_header=True,
|
should_write_header=True,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Receives params from the parent methods and outputs a CSV with fltered and sorted domains.
|
Receives params from the parent methods and outputs a CSV with filtered and sorted domains.
|
||||||
Works with write_header as longas the same writer object is passed.
|
Works with write_header as long as the same writer object is passed.
|
||||||
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
|
||||||
should_write_header: Conditional bc export_data_growth_to_csv calls write_body twice
|
should_write_header: Conditional bc export_data_domain_growth_to_csv calls write_body twice
|
||||||
"""
|
"""
|
||||||
|
|
||||||
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||||
|
@ -175,7 +183,7 @@ def write_csv(
|
||||||
columns.append(column_name)
|
columns.append(column_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
row = parse_domain_row(columns, domain_info, security_emails_dict, get_domain_managers)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# This should not happen. If it does, just skip this row.
|
# This should not happen. If it does, just skip this row.
|
||||||
|
@ -189,6 +197,82 @@ def write_csv(
|
||||||
writer.writerows(total_body_rows)
|
writer.writerows(total_body_rows)
|
||||||
|
|
||||||
|
|
||||||
|
def get_requests(filter_condition, sort_fields):
|
||||||
|
"""
|
||||||
|
Returns DomainRequest objects filtered and sorted based on the provided conditions.
|
||||||
|
filter_condition -> A dictionary of conditions to filter the objects.
|
||||||
|
sort_fields -> A list of fields to sort the resulting query set.
|
||||||
|
returns: A queryset of DomainRequest objects
|
||||||
|
"""
|
||||||
|
requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request_row(columns, request: DomainRequest):
|
||||||
|
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||||
|
|
||||||
|
requested_domain_name = "No requested domain"
|
||||||
|
|
||||||
|
if request.requested_domain is not None:
|
||||||
|
requested_domain_name = request.requested_domain.name
|
||||||
|
|
||||||
|
if request.federal_type:
|
||||||
|
request_type = f"{request.get_organization_type_display()} - {request.get_federal_type_display()}"
|
||||||
|
else:
|
||||||
|
request_type = request.get_organization_type_display()
|
||||||
|
|
||||||
|
# create a dictionary of fields which can be included in output
|
||||||
|
FIELDS = {
|
||||||
|
"Requested domain": requested_domain_name,
|
||||||
|
"Status": request.get_status_display(),
|
||||||
|
"Organization type": request_type,
|
||||||
|
"Agency": request.federal_agency,
|
||||||
|
"Organization name": request.organization_name,
|
||||||
|
"City": request.city,
|
||||||
|
"State": request.state_territory,
|
||||||
|
"AO email": request.authorizing_official.email if request.authorizing_official else " ",
|
||||||
|
"Security contact email": request,
|
||||||
|
"Created at": request.created_at,
|
||||||
|
"Submission date": request.submission_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
row = [FIELDS.get(column, "") for column in columns]
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def write_requests_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_condition,
|
||||||
|
should_write_header=True,
|
||||||
|
):
|
||||||
|
"""Receives params from the parent methods and outputs a CSV with filtered and sorted requests.
|
||||||
|
Works with write_header as long as the same writer object is passed."""
|
||||||
|
|
||||||
|
all_requests = get_requests(filter_condition, sort_fields)
|
||||||
|
|
||||||
|
# Reduce the memory overhead when performing the write operation
|
||||||
|
paginator = Paginator(all_requests, 1000)
|
||||||
|
|
||||||
|
for page_num in paginator.page_range:
|
||||||
|
page = paginator.page(page_num)
|
||||||
|
rows = []
|
||||||
|
for request in page.object_list:
|
||||||
|
try:
|
||||||
|
row = parse_request_row(columns, request)
|
||||||
|
rows.append(row)
|
||||||
|
except ValueError:
|
||||||
|
# This should not happen. If it does, just skip this row.
|
||||||
|
# It indicates that DomainInformation.domain is None.
|
||||||
|
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if should_write_header:
|
||||||
|
write_header(writer, columns)
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
def export_data_type_to_csv(csv_file):
|
def export_data_type_to_csv(csv_file):
|
||||||
"""All domains report with extra columns"""
|
"""All domains report with extra columns"""
|
||||||
|
|
||||||
|
@ -223,7 +307,9 @@ def export_data_type_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def export_data_full_to_csv(csv_file):
|
def export_data_full_to_csv(csv_file):
|
||||||
|
@ -254,7 +340,9 @@ def export_data_full_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def export_data_federal_to_csv(csv_file):
|
def export_data_federal_to_csv(csv_file):
|
||||||
|
@ -286,7 +374,9 @@ def export_data_federal_to_csv(csv_file):
|
||||||
Domain.State.ON_HOLD,
|
Domain.State.ON_HOLD,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_start_date():
|
def get_default_start_date():
|
||||||
|
@ -299,7 +389,15 @@ def get_default_end_date():
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
|
|
||||||
def export_data_growth_to_csv(csv_file, start_date, end_date):
|
def format_start_date(start_date):
|
||||||
|
return timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) if start_date else get_default_start_date()
|
||||||
|
|
||||||
|
|
||||||
|
def format_end_date(end_date):
|
||||||
|
return timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_domain_growth_to_csv(csv_file, start_date, end_date):
|
||||||
"""
|
"""
|
||||||
Growth report:
|
Growth report:
|
||||||
Receive start and end dates from the view, parse them.
|
Receive start and end dates from the view, parse them.
|
||||||
|
@ -308,16 +406,9 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
the start and end dates. Specify sort params for both lists.
|
the start and end dates. Specify sort params for both lists.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
start_date_formatted = (
|
start_date_formatted = format_start_date(start_date)
|
||||||
timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) if start_date else get_default_start_date()
|
end_date_formatted = format_end_date(end_date)
|
||||||
)
|
|
||||||
|
|
||||||
end_date_formatted = (
|
|
||||||
timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
|
|
||||||
)
|
|
||||||
|
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
# define columns to include in export
|
# define columns to include in export
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
|
@ -353,8 +444,10 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
"domain__deleted__gte": start_date_formatted,
|
"domain__deleted__gte": start_date_formatted,
|
||||||
}
|
}
|
||||||
|
|
||||||
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
|
write_domains_csv(
|
||||||
write_csv(
|
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
|
||||||
|
)
|
||||||
|
write_domains_csv(
|
||||||
writer,
|
writer,
|
||||||
columns,
|
columns,
|
||||||
sort_fields_for_deleted_domains,
|
sort_fields_for_deleted_domains,
|
||||||
|
@ -362,3 +455,266 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
|
||||||
get_domain_managers=False,
|
get_domain_managers=False,
|
||||||
should_write_header=False,
|
should_write_header=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sliced_domains(filter_condition, distinct=False):
|
||||||
|
"""Get filtered domains counts sliced by org type and election office.
|
||||||
|
Pass distinct=True when filtering by permissions so we do not to count multiples
|
||||||
|
when a domain has more that one manager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Round trip 1: Get distinct domain names based on filter condition
|
||||||
|
domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count()
|
||||||
|
|
||||||
|
# Round trip 2: Get counts for other slices
|
||||||
|
if distinct:
|
||||||
|
organization_types_query = (
|
||||||
|
DomainInformation.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
organization_types_query = DomainInformation.objects.filter(**filter_condition).values_list(
|
||||||
|
"organization_type", flat=True
|
||||||
|
)
|
||||||
|
organization_type_counts = Counter(organization_types_query)
|
||||||
|
|
||||||
|
federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||||
|
interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||||
|
state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||||
|
tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||||
|
county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||||
|
city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||||
|
special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||||
|
school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||||
|
|
||||||
|
# Round trip 3
|
||||||
|
election_board = DomainInformation.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||||
|
|
||||||
|
return [
|
||||||
|
domains_count,
|
||||||
|
federal,
|
||||||
|
interstate,
|
||||||
|
state_or_territory,
|
||||||
|
tribal,
|
||||||
|
county,
|
||||||
|
city,
|
||||||
|
special_district,
|
||||||
|
school_district,
|
||||||
|
election_board,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_sliced_requests(filter_condition, distinct=False):
|
||||||
|
"""Get filtered requests counts sliced by org type and election office."""
|
||||||
|
|
||||||
|
# Round trip 1: Get distinct requests based on filter condition
|
||||||
|
requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count()
|
||||||
|
|
||||||
|
# Round trip 2: Get counts for other slices
|
||||||
|
if distinct:
|
||||||
|
organization_types_query = (
|
||||||
|
DomainRequest.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
organization_types_query = DomainRequest.objects.filter(**filter_condition).values_list(
|
||||||
|
"organization_type", flat=True
|
||||||
|
)
|
||||||
|
organization_type_counts = Counter(organization_types_query)
|
||||||
|
|
||||||
|
federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0)
|
||||||
|
interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0)
|
||||||
|
state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0)
|
||||||
|
tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0)
|
||||||
|
county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0)
|
||||||
|
city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0)
|
||||||
|
special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0)
|
||||||
|
school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0)
|
||||||
|
|
||||||
|
# Round trip 3
|
||||||
|
election_board = DomainRequest.objects.filter(is_election_board=True, **filter_condition).distinct().count()
|
||||||
|
|
||||||
|
return [
|
||||||
|
requests_count,
|
||||||
|
federal,
|
||||||
|
interstate,
|
||||||
|
state_or_territory,
|
||||||
|
tribal,
|
||||||
|
county,
|
||||||
|
city,
|
||||||
|
special_district,
|
||||||
|
school_district,
|
||||||
|
election_board,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_managed_domains_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""Get counts for domains that have domain managers for two different dates,
|
||||||
|
get list of managed domains at end_date."""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
filter_managed_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_start_date = get_sliced_domains(filter_managed_domains_start_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["MANAGED DOMAINS COUNTS AT START DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(managed_domains_sliced_at_start_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
filter_managed_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_end_date = get_sliced_domains(filter_managed_domains_end_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["MANAGED DOMAINS COUNTS AT END DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(managed_domains_sliced_at_end_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
write_domains_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_managed_domains_end_date,
|
||||||
|
get_domain_managers=True,
|
||||||
|
should_write_header=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""Get counts for domains that do not have domain managers for two different dates,
|
||||||
|
get list of unmanaged domains at end_date."""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
columns = [
|
||||||
|
"Domain name",
|
||||||
|
"Domain type",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"domain__name",
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_unmanaged_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_start_date = get_sliced_domains(filter_unmanaged_domains_start_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["UNMANAGED DOMAINS AT START DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(unmanaged_domains_sliced_at_start_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
filter_unmanaged_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_end_date = get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||||
|
|
||||||
|
writer.writerow(["UNMANAGED DOMAINS AT END DATE"])
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"Federal",
|
||||||
|
"Interstate",
|
||||||
|
"State or territory",
|
||||||
|
"Tribal",
|
||||||
|
"County",
|
||||||
|
"City",
|
||||||
|
"Special district",
|
||||||
|
"School district",
|
||||||
|
"Election office",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
writer.writerow(unmanaged_domains_sliced_at_end_date)
|
||||||
|
writer.writerow([])
|
||||||
|
|
||||||
|
write_domains_csv(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
sort_fields,
|
||||||
|
filter_unmanaged_domains_end_date,
|
||||||
|
get_domain_managers=False,
|
||||||
|
should_write_header=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
|
||||||
|
"""
|
||||||
|
Growth report:
|
||||||
|
Receive start and end dates from the view, parse them.
|
||||||
|
Request from write_requests_body SUBMITTED requests that are created between
|
||||||
|
the start and end dates. Specify sort params.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_date_formatted = format_start_date(start_date)
|
||||||
|
end_date_formatted = format_end_date(end_date)
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
# define columns to include in export
|
||||||
|
columns = [
|
||||||
|
"Requested domain",
|
||||||
|
"Organization type",
|
||||||
|
"Submission date",
|
||||||
|
]
|
||||||
|
sort_fields = [
|
||||||
|
"requested_domain__name",
|
||||||
|
]
|
||||||
|
filter_condition = {
|
||||||
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": end_date_formatted,
|
||||||
|
"submission_date__gte": start_date_formatted,
|
||||||
|
}
|
||||||
|
|
||||||
|
write_requests_csv(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.db.models import Avg, F
|
||||||
|
from .. import models
|
||||||
|
import datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from registrar.utility import csv_export
|
from registrar.utility import csv_export
|
||||||
|
|
||||||
|
@ -10,7 +16,157 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ExportData(View):
|
class AnalyticsView(View):
|
||||||
|
def get(self, request):
|
||||||
|
thirty_days_ago = datetime.datetime.today() - datetime.timedelta(days=30)
|
||||||
|
thirty_days_ago = timezone.make_aware(thirty_days_ago)
|
||||||
|
|
||||||
|
last_30_days_applications = models.DomainRequest.objects.filter(created_at__gt=thirty_days_ago)
|
||||||
|
last_30_days_approved_applications = models.DomainRequest.objects.filter(
|
||||||
|
created_at__gt=thirty_days_ago, status=models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
)
|
||||||
|
avg_approval_time = last_30_days_approved_applications.annotate(
|
||||||
|
approval_time=F("approved_domain__created_at") - F("submission_date")
|
||||||
|
).aggregate(Avg("approval_time"))["approval_time__avg"]
|
||||||
|
# Format the timedelta to display only days
|
||||||
|
if avg_approval_time is not None:
|
||||||
|
avg_approval_time_display = f"{avg_approval_time.days} days"
|
||||||
|
else:
|
||||||
|
avg_approval_time_display = "No approvals to use"
|
||||||
|
|
||||||
|
# The start and end dates are passed as url params
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
|
start_date_formatted = csv_export.format_start_date(start_date)
|
||||||
|
end_date_formatted = csv_export.format_end_date(end_date)
|
||||||
|
|
||||||
|
filter_managed_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_managed_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": False,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date, True)
|
||||||
|
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date, True)
|
||||||
|
|
||||||
|
filter_unmanaged_domains_start_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_unmanaged_domains_end_date = {
|
||||||
|
"domain__permissions__isnull": True,
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(
|
||||||
|
filter_unmanaged_domains_start_date, True
|
||||||
|
)
|
||||||
|
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date, True)
|
||||||
|
|
||||||
|
filter_ready_domains_start_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
|
"domain__first_ready__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_ready_domains_end_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
ready_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_ready_domains_start_date)
|
||||||
|
ready_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_ready_domains_end_date)
|
||||||
|
|
||||||
|
filter_deleted_domains_start_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
|
"domain__deleted__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_deleted_domains_end_date = {
|
||||||
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
|
"domain__deleted__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
deleted_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_deleted_domains_start_date)
|
||||||
|
deleted_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_deleted_domains_end_date)
|
||||||
|
|
||||||
|
filter_requests_start_date = {
|
||||||
|
"created_at__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_requests_end_date = {
|
||||||
|
"created_at__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_requests_start_date)
|
||||||
|
requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_requests_end_date)
|
||||||
|
|
||||||
|
filter_submitted_requests_start_date = {
|
||||||
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": start_date_formatted,
|
||||||
|
}
|
||||||
|
filter_submitted_requests_end_date = {
|
||||||
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
"submission_date__lte": end_date_formatted,
|
||||||
|
}
|
||||||
|
submitted_requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_submitted_requests_start_date)
|
||||||
|
submitted_requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_submitted_requests_end_date)
|
||||||
|
|
||||||
|
context = dict(
|
||||||
|
# Generate a dictionary of context variables that are common across all admin templates
|
||||||
|
# (site_header, site_url, ...),
|
||||||
|
# include it in the larger context dictionary so it's available in the template rendering context.
|
||||||
|
# This ensures that the admin interface styling and behavior are consistent with other admin pages.
|
||||||
|
**admin.site.each_context(request),
|
||||||
|
data=dict(
|
||||||
|
user_count=models.User.objects.all().count(),
|
||||||
|
domain_count=models.Domain.objects.all().count(),
|
||||||
|
ready_domain_count=models.Domain.objects.filter(state=models.Domain.State.READY).count(),
|
||||||
|
last_30_days_applications=last_30_days_applications.count(),
|
||||||
|
last_30_days_approved_applications=last_30_days_approved_applications.count(),
|
||||||
|
average_application_approval_time_last_30_days=avg_approval_time_display,
|
||||||
|
managed_domains_sliced_at_start_date=managed_domains_sliced_at_start_date,
|
||||||
|
unmanaged_domains_sliced_at_start_date=unmanaged_domains_sliced_at_start_date,
|
||||||
|
managed_domains_sliced_at_end_date=managed_domains_sliced_at_end_date,
|
||||||
|
unmanaged_domains_sliced_at_end_date=unmanaged_domains_sliced_at_end_date,
|
||||||
|
ready_domains_sliced_at_start_date=ready_domains_sliced_at_start_date,
|
||||||
|
deleted_domains_sliced_at_start_date=deleted_domains_sliced_at_start_date,
|
||||||
|
ready_domains_sliced_at_end_date=ready_domains_sliced_at_end_date,
|
||||||
|
deleted_domains_sliced_at_end_date=deleted_domains_sliced_at_end_date,
|
||||||
|
requests_sliced_at_start_date=requests_sliced_at_start_date,
|
||||||
|
submitted_requests_sliced_at_start_date=submitted_requests_sliced_at_start_date,
|
||||||
|
requests_sliced_at_end_date=requests_sliced_at_end_date,
|
||||||
|
submitted_requests_sliced_at_end_date=submitted_requests_sliced_at_end_date,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return render(request, "admin/analytics.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataType(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataFull(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataFederal(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataDomainsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
# Get start_date and end_date from the request's GET parameters
|
||||||
# #999: not needed if we switch to django forms
|
# #999: not needed if we switch to django forms
|
||||||
|
@ -19,8 +175,50 @@ class ExportData(View):
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
||||||
# For #999: set export_data_growth_to_csv to return the resulting queryset, which we can then use
|
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||||
# in context to display this data in the template.
|
# in context to display this data in the template.
|
||||||
csv_export.export_data_growth_to_csv(response, start_date, end_date)
|
csv_export.export_data_domain_growth_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataRequestsGrowth(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
|
||||||
|
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
||||||
|
# in context to display this data in the template.
|
||||||
|
csv_export.export_data_requests_growth_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataManagedDomains(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
|
||||||
|
csv_export.export_data_managed_domains_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataUnmanagedDomains(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
# Get start_date and end_date from the request's GET parameters
|
||||||
|
# #999: not needed if we switch to django forms
|
||||||
|
start_date = request.GET.get("start_date", "")
|
||||||
|
end_date = request.GET.get("end_date", "")
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="unamanaged-domains-{start_date}-to-{end_date}.csv"'
|
||||||
|
csv_export.export_data_unmanaged_domains_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue