Skip to content

Commit

Permalink
Merge pull request #1867 from cisagov/rjm/1789-metrics-report
Browse files Browse the repository at this point in the history
Issue #1789: Analytics dashboard
  • Loading branch information
rachidatecs authored Mar 19, 2024
2 parents 3a189c2 + 4e1c239 commit 7a6beba
Show file tree
Hide file tree
Showing 20 changed files with 1,412 additions and 532 deletions.
70 changes: 10 additions & 60 deletions src/registrar/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import copy

from django import forms
from django.db.models.functions import Concat, Coalesce
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_fsm import get_available_FIELD_transitions
from django.contrib import admin, messages
Expand All @@ -16,7 +16,6 @@
from dateutil.relativedelta import relativedelta # type: ignore
from epplibwrapper.errors import ErrorCode, RegistryError
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.views.utility.mixins import OrderableFieldsMixin
from django.contrib.admin.views.main import ORDER_VAR
Expand Down Expand Up @@ -1465,7 +1464,6 @@ def state_territory(self, obj):
search_fields = ["name"]
search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html"
change_list_template = "django/admin/domain_change_list.html"
readonly_fields = ["state", "expiration_date", "first_ready", "deleted"]

# Table ordering
Expand Down Expand Up @@ -1512,56 +1510,6 @@ def changeform_view(self, request, object_id=None, form_url="", extra_context=No

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):
# Create dictionary of action functions
ACTION_FUNCTIONS = {
Expand Down Expand Up @@ -1693,9 +1641,11 @@ def do_delete_domain(self, request, obj):
else:
self.message_user(
request,
"Error deleting this Domain: "
f"Can't switch from state '{obj.state}' to 'deleted'"
", must be either 'dns_needed' or 'on_hold'",
(
"Error deleting this Domain: "
f"Can't switch from state '{obj.state}' to 'deleted'"
", must be either 'dns_needed' or 'on_hold'"
),
messages.ERROR,
)
except Exception:
Expand All @@ -1707,7 +1657,7 @@ def do_delete_domain(self, request, obj):
else:
self.message_user(
request,
("Domain %s has been deleted. Thanks!") % obj.name,
"Domain %s has been deleted. Thanks!" % obj.name,
)

return HttpResponseRedirect(".")
Expand Down Expand Up @@ -1749,7 +1699,7 @@ def do_place_client_hold(self, request, obj):
else:
self.message_user(
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(".")

Expand Down Expand Up @@ -1778,7 +1728,7 @@ def do_remove_client_hold(self, request, obj):
else:
self.message_user(
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(".")

Expand Down
37 changes: 0 additions & 37 deletions src/registrar/assets/js/get-gov-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,43 +368,6 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
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
* status select amd to show/hide the rejection reason
*/
Expand Down
117 changes: 117 additions & 0 deletions src/registrar/assets/js/get-gov-reports.js
Original file line number Diff line number Diff line change
@@ -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,
});
}
34 changes: 32 additions & 2 deletions src/registrar/assets/sass/_theme/_admin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ html[data-theme="light"] {
.change-list .usa-table thead th,
body.dashboard,
body.change-list,
body.change-form {
body.change-form,
.analytics {
color: var(--body-fg);
}
}
Expand Down Expand Up @@ -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;
// Styling based off of the <p> styling in django admin
line-height: 1.5;
Expand Down
5 changes: 3 additions & 2 deletions src/registrar/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,9 @@

# Google analytics requires that we relax our otherwise
# strict CSP by allowing scripts to run from 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/"]
# and inline with a nonce, as well as allowing connections back to their domain.
# 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_INCLUDE_NONCE_IN = ["script-src-elem"]

Expand Down
Loading

0 comments on commit 7a6beba

Please sign in to comment.