diff --git a/src/registrar/admin.py b/src/registrar/admin.py index def7c64b1..9a8a655c1 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -795,6 +795,9 @@ class DomainAdmin(ListHeaderAdmin): "name", "organization_type", "state", + "created_at", + "deleted_at", + "expiration_date", ] def organization_type(self, obj): @@ -809,7 +812,7 @@ class DomainAdmin(ListHeaderAdmin): 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"] + readonly_fields = ["state", "expiration_date", "deleted_at"] def export_data_type(self, request): # match the CSV example with all the fields diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 53eeb22a3..dcdeeb106 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -275,3 +275,54 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk)); viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText)); } + +// function performDataLookup(e) { +// e.preventDefault(); // Prevent the default form submission + +// console.log('Form submitted!'); + + +// var form = document.getElementById("exportDataForm"); +// var formData = new FormData(form); + +// // Perform an AJAX request to fetch data +// fetch('/admin/', { +// method: 'POST', +// body: formData, +// }) +// .then(response => { +// if (!response.ok) { +// console.log(response); +// console.log(`HTTP error! Status: ${response.status}`); +// throw new Error(`HTTP error! Status: ${response.status}`); +// } +// return response.json(); +// }) +// .then(data => { +// // Handle the data (update the result div, for example) +// document.getElementById("dataResult").innerText = JSON.stringify(data); +// }) +// .catch(error => console.error('Error:', error)); +// } + + (function (){ + + document.getElementById('exportLink').addEventListener('click', function(event) { + event.preventDefault(); // Prevent the default link behavior + + // Get the selected start and end dates + var startDate = document.getElementById('start').value; + var endDate = document.getElementById('end').value; + + var 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; + }); + + + // document.getElementById('exportDataForm').addEventListener('submit', performDataLookup); +})(); \ No newline at end of file diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index c99daf72b..317cb9375 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -210,7 +210,7 @@ STATICFILES_DIRS = [ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "registrar" / "templates"], + # "DIRS": [BASE_DIR / "registrar" / "templates"], # look for templates inside installed apps # required by django-debug-toolbar "APP_DIRS": True, diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index d30c85ce9..af44fc237 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -9,6 +9,10 @@ from django.urls import include, path from django.views.generic import RedirectView from registrar import views +# from registrar.views.admin_views import export_data +from registrar.views.admin_views import ExportData + + from registrar.views.application import Step from registrar.views.utility import always_404 from api.views import available, get_current_federal, get_current_full @@ -50,6 +54,8 @@ urlpatterns = [ RedirectView.as_view(pattern_name="logout", permanent=False), ), path("admin/", admin.site.urls), + # path('export_data/', export_data, name='admin_export_data'), + path('export_data/', ExportData.as_view(), name='admin_export_data'), path( "application//edit/", views.ApplicationWizard.as_view(), diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 914db375c..a1c34e777 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -10,3 +10,4 @@ from .domain import ( DomainDsdataFormset, DomainDsdataForm, ) +from .admin import DataExportForm diff --git a/src/registrar/forms/admin.py b/src/registrar/forms/admin.py new file mode 100644 index 000000000..78d359743 --- /dev/null +++ b/src/registrar/forms/admin.py @@ -0,0 +1,13 @@ +from django import forms + +class DataExportForm(forms.Form): + # start_date = forms.DateField(label='Start date', widget=forms.DateInput(attrs={'type': 'date'})) + # end_date = forms.DateField(label='End date', widget=forms.DateInput(attrs={'type': 'date'})) + + security_email = forms.EmailField( + label="Security email (optional)", + required=False, + error_messages={ + "invalid": 'dsas', + }, + ) \ No newline at end of file diff --git a/src/registrar/migrations/0057_domain_deleted_at.py b/src/registrar/migrations/0057_domain_deleted_at.py new file mode 100644 index 000000000..e93068945 --- /dev/null +++ b/src/registrar/migrations/0057_domain_deleted_at.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.7 on 2023-12-19 05:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0056_alter_domain_state_alter_domainapplication_status_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="domain", + name="deleted_at", + field=models.DateField(editable=False, help_text="Deleted at date", null=True), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 44cb45433..25c60ca2a 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -33,6 +33,7 @@ from django.db.models import DateField from .utility.domain_field import DomainField from .utility.domain_helper import DomainHelper from .utility.time_stamped_model import TimeStampedModel +from django.utils import timezone from .public_contact import PublicContact @@ -959,6 +960,12 @@ class Domain(TimeStampedModel, DomainHelper): null=True, help_text=("Duplication of registry's expiration date saved for ease of reporting"), ) + + deleted_at = DateField( + null=True, + editable=False, + help_text="Deleted at date", + ) def isActive(self): return self.state == Domain.State.CREATED @@ -1279,6 +1286,8 @@ class Domain(TimeStampedModel, DomainHelper): try: logger.info("deletedInEpp()-> inside _delete_domain") self._delete_domain() + self.deleted_at = timezone.now() + self.save() except RegistryError as err: logger.error(f"Could not delete domain. Registry returned error: {err}") raise err diff --git a/src/registrar/templates/admin/export_data.html b/src/registrar/templates/admin/export_data.html new file mode 100644 index 000000000..589284c1b --- /dev/null +++ b/src/registrar/templates/admin/export_data.html @@ -0,0 +1,18 @@ + +
+ {% csrf_token %} + + + {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.errors %} +
{{ field.errors|join:", " }}
+ {% endif %} +
+ {% endfor %} + + + +
\ No newline at end of file diff --git a/src/registrar/templates/admin/index.html b/src/registrar/templates/admin/index.html new file mode 100644 index 000000000..82c881a9e --- /dev/null +++ b/src/registrar/templates/admin/index.html @@ -0,0 +1,20 @@ +{% extends "admin/index.html" %} + +{% block content %} +
+ {% include "admin/app_list.html" with app_list=app_list show_changelinks=True %} +
+

Welcome to the Custom Admin Homepage!

+ + {% comment %} {% include "export_data.html" %} {% endcomment %} + + + + + + + Export + +
+
+{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/export_data.html b/src/registrar/templates/export_data.html new file mode 100644 index 000000000..69b00c744 --- /dev/null +++ b/src/registrar/templates/export_data.html @@ -0,0 +1,19 @@ +{% load static field_helpers%} + +
+ {% csrf_token %} + + + {% for field in form %} +
+ {{ field.label_tag }} + {{ field }} + {% if field.errors %} +
{{ field.errors|join:", " }}
+ {% endif %} +
+ {% endfor %} + + + +
\ No newline at end of file diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 64136c3a5..5ceb49cd2 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -1,4 +1,5 @@ import csv +from datetime import datetime from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation from registrar.models.public_contact import PublicContact @@ -10,9 +11,18 @@ def export_domains_to_writer(writer, columns, sort_fields, filter_condition): # write columns headers to writer writer.writerow(columns) - domainInfos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) + + print(f"filter_condition {filter_condition}") + if 'domain__created_at__gt' in filter_condition: + + domainInfos = DomainInformation.objects.filter(domain__state=Domain.State.DELETED).order_by("domain__deleted_at") + print(f"filtering by deleted {domainInfos}") + else: + domainInfos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) + for domainInfo in domainInfos: security_contacts = domainInfo.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) + print(f"regular filtering {domainInfos}") # For linter ao = " " if domainInfo.authorizing_official: @@ -31,9 +41,11 @@ def export_domains_to_writer(writer, columns, sort_fields, filter_condition): "State": domainInfo.state_territory, "AO": ao, "AO email": domainInfo.authorizing_official.email if domainInfo.authorizing_official else " ", - "Security Contact Email": security_contacts[0].email if security_contacts else " ", + "Security contact email": security_contacts[0].email if security_contacts else " ", "Status": domainInfo.domain.state, - "Expiration Date": domainInfo.domain.expiration_date, + "Expiration date": domainInfo.domain.expiration_date, + "Created at": domainInfo.domain.created_at, + "Deleted at": domainInfo.domain.deleted_at, } writer.writerow([FIELDS.get(column, "") for column in columns]) @@ -50,9 +62,9 @@ def export_data_type_to_csv(csv_file): "State", "AO", "AO email", - "Security Contact Email", + "Security contact email", "Status", - "Expiration Date", + "Expiration date", ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ @@ -81,7 +93,7 @@ def export_data_full_to_csv(csv_file): "Organization name", "City", "State", - "Security Contact Email", + "Security contact email", ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ @@ -110,7 +122,7 @@ def export_data_federal_to_csv(csv_file): "Organization name", "City", "State", - "Security Contact Email", + "Security contact email", ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ @@ -128,3 +140,53 @@ def export_data_federal_to_csv(csv_file): ], } export_domains_to_writer(writer, columns, sort_fields, filter_condition) + +def export_data_growth_to_csv(csv_file, start_date, end_date): + + print(f'start_date {start_date}') + print(f'end_date {end_date}') + + # Check if start_date is not empty before using strptime + if start_date: + start_date_formatted = datetime.strptime(start_date, "%Y-%m-%d") + print(f'start_date_formatted {start_date_formatted}') + else: + # Handle the case where start_date is missing or empty + print('ON NO') + start_date_formatted = None # Replace with appropriate handling + + if end_date: + end_date_formatted = datetime.strptime(end_date, "%Y-%m-%d") + print(f'start_date_formatted {end_date_formatted}') + else: + # Handle the case where start_date is missing or empty + print('ON NO') + end_date_formatted = None # Replace with appropriate handling + + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + "Created at", + "Expiration date", + ] + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "created_at", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.UNKNOWN, + Domain.State.DELETED, + ], + "domain__expiration_date__lt": end_date_formatted, + "domain__created_at__gt": start_date_formatted, + } + export_domains_to_writer(writer, columns, sort_fields, filter_condition) diff --git a/src/registrar/views/admin_views.py b/src/registrar/views/admin_views.py new file mode 100644 index 000000000..6c6aa6616 --- /dev/null +++ b/src/registrar/views/admin_views.py @@ -0,0 +1,74 @@ +"""Admin-related views.""" + +from django.http import HttpResponse, JsonResponse +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.shortcuts import render + +from registrar.utility import csv_export +from ..forms import DataExportForm +from django.views.generic import TemplateView + +from registrar.models import ( + Domain, + DomainApplication, + DomainInvitation, + DomainInformation, + UserDomainRole, +) +import logging + + +logger = logging.getLogger(__name__) + +def export_data(self): + """CSV download""" + print('VIEW') + # Federal only + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = 'attachment; filename="current-federal.csv"' + csv_export.export_data_growth_to_csv(response) + return response + +class ExportData(View): + + def get(self, request, *args, **kwargs): + # Get start_date and end_date from the request's GET parameters + start_date = request.GET.get('start_date', '') + end_date = request.GET.get('end_date', '') + + print(start_date) + print(end_date) + # Do something with start_date and end_date, e.g., include in the CSV export logic + + # # Federal only + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = f'attachment; filename="growth-from-{start_date}-to-{end_date}.csv"' + csv_export.export_data_growth_to_csv(response, start_date, end_date) + + + # response = HttpResponse(content_type="text/csv") + # response["Content-Disposition"] = 'attachment; filename="current-federal.csv"' + # csv_export.export_data_growth_to_csv(response) + + return response + + +# class ExportData(TemplateView): +# """Django form""" + +# template_name = "export_data.html" +# form_class = DataExportForm + +# def form_valid(self, form): +# print('Form is valid') +# # Form is valid, perform data export logic here +# return JsonResponse({'message': 'Data exported successfully!'}, content_type='application/json') + +# def form_invalid(self, form): +# print('Form is invalid') +# # Form is invalid, return error response +# return JsonResponse({'error': 'Invalid form data'}, status=400, content_type='application/json') + + + \ No newline at end of file