This commit is contained in:
Rachid Mrad 2023-12-21 12:46:26 -05:00
parent 4b38c4abc8
commit ff32a02022
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
7 changed files with 126 additions and 77 deletions

View file

@ -9,6 +9,7 @@ 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 export_data # from registrar.views.admin_views import export_data
from registrar.views.admin_views import ExportData from registrar.views.admin_views import ExportData
@ -53,7 +54,7 @@ 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("export_data/", ExportData.as_view(), name="admin_export_data"),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path( path(
"application/<id>/edit/", "application/<id>/edit/",

View file

@ -960,13 +960,13 @@ class Domain(TimeStampedModel, DomainHelper):
null=True, null=True,
help_text=("Duplication of registry's expiration date saved for ease of reporting"), help_text=("Duplication of registry's expiration date saved for ease of reporting"),
) )
deleted_at = DateField( deleted_at = DateField(
null=True, null=True,
editable=False, editable=False,
help_text="Deleted at date", help_text="Deleted at date",
) )
ready_at = DateField( ready_at = DateField(
null=True, null=True,
editable=False, editable=False,

View file

@ -1,7 +1,6 @@
from django.test import TestCase, Client from django.test import TestCase, Client
from django.urls import reverse from django.urls import reverse
from registrar.tests.common import create_superuser from registrar.tests.common import create_superuser
from registrar.views.admin_views import ExportData
class TestViews(TestCase): class TestViews(TestCase):
@ -10,16 +9,15 @@ class TestViews(TestCase):
self.superuser = create_superuser() self.superuser = create_superuser()
def test_export_data_view(self): def test_export_data_view(self):
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
# Reverse the URL for the admin index page # Reverse the URL for the admin index page
admin_index_url = reverse("admin:index") admin_index_url = reverse("admin:index")
# Make a GET request to the admin index page # Make a GET request to the admin index page
response = self.client.get(admin_index_url) response = self.client.get(admin_index_url)
print(f'response1 {response}') print(f"response1 {response}")
# Assert that the response status code is 200 (OK) # Assert that the response status code is 200 (OK)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -39,14 +37,10 @@ class TestViews(TestCase):
# Assert that the response status code is 200 (OK) or the expected status code # Assert that the response status code is 200 (OK) or the expected status code
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Assert that the content type is CSV # Assert that the content type is CSV
self.assertEqual(response["Content-Type"], "text/csv") self.assertEqual(response["Content-Type"], "text/csv")
# Check if the filename in the Content-Disposition header matches the expected pattern # Check if the filename in the Content-Disposition header matches the expected pattern
expected_filename = f'growth-from-{start_date}-to-{end_date}.csv' expected_filename = f"growth-from-{start_date}-to-{end_date}.csv"
self.assertIn(f'attachment; filename="{expected_filename}"', response["Content-Disposition"]) self.assertIn(f'attachment; filename="{expected_filename}"', response["Content-Disposition"])

View file

@ -1138,7 +1138,7 @@ class TestRegistrantNameservers(MockEppLib):
# check that status is still NOT READY # check that status is still NOT READY
# as you have less than 2 nameservers # as you have less than 2 nameservers
self.assertFalse(self.domain.is_active()) self.assertFalse(self.domain.is_active())
self.assertEqual(self.domain.ready_at, None) self.assertEqual(self.domain.ready_at, None)
def test_user_adds_two_nameservers(self): def test_user_adds_two_nameservers(self):
@ -2253,7 +2253,7 @@ class TestAnalystDelete(MockEppLib):
When `domain.deletedInEpp()` is called When `domain.deletedInEpp()` is called
Then `commands.DeleteDomain` is sent to the registry Then `commands.DeleteDomain` is sent to the registry
And `state` is set to `DELETED` And `state` is set to `DELETED`
The deleted_at date is set. The deleted_at date is set.
""" """
# Put the domain in client hold # Put the domain in client hold
@ -2275,7 +2275,7 @@ class TestAnalystDelete(MockEppLib):
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.DELETED) self.assertEqual(self.domain.state, Domain.State.DELETED)
# Domain should have a deleted_at # Domain should have a deleted_at
self.assertNotEqual(self.domain.deleted_at, None) self.assertNotEqual(self.domain.deleted_at, None)
@ -2321,7 +2321,7 @@ class TestAnalystDelete(MockEppLib):
and domain is of `state` is `READY` and domain is of `state` is `READY`
Then an FSM error is returned Then an FSM error is returned
And `state` is not set to `DELETED` And `state` is not set to `DELETED`
The deleted_at date is still null. The deleted_at date is still null.
""" """
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
@ -2333,6 +2333,6 @@ class TestAnalystDelete(MockEppLib):
self.assertNotEqual(self.domain, None) self.assertNotEqual(self.domain, None)
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
# deleted_at should be null # deleted_at should be null
self.assertEqual(self.domain.deleted_at, None) self.assertEqual(self.domain.deleted_at, None)

View file

@ -6,7 +6,11 @@ from registrar.models.domain_information import DomainInformation
from registrar.models.domain import Domain from registrar.models.domain import Domain
from registrar.models.user import User from registrar.models.user import User
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from registrar.utility.csv_export import export_domains_to_writer, get_default_start_date, get_default_end_date, export_data_growth_to_csv from registrar.utility.csv_export import (
export_domains_to_writer,
get_default_start_date,
get_default_end_date,
)
from django.core.management import call_command from django.core.management import call_command
from unittest.mock import MagicMock, call, mock_open, patch from unittest.mock import MagicMock, call, mock_open, patch
from api.views import get_current_federal, get_current_full from api.views import get_current_federal, get_current_full
@ -17,6 +21,7 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.utils import timezone from django.utils import timezone
class CsvReportsTest(TestCase): class CsvReportsTest(TestCase):
"""Tests to determine if we are uploading our reports correctly""" """Tests to determine if we are uploading our reports correctly"""
@ -227,21 +232,40 @@ class ExportDataTest(TestCase):
username=username, first_name=first_name, last_name=last_name, email=email 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, ready_at=timezone.now()) self.domain_1, _ = Domain.objects.get_or_create(
name="cdomain1.gov", state=Domain.State.READY, ready_at=timezone.now()
)
self.domain_2, _ = Domain.objects.get_or_create(name="adomain2.gov", state=Domain.State.DNS_NEEDED) 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_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_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_at=timezone.make_aware(datetime(2023, 11, 1))) self.domain_5, _ = Domain.objects.get_or_create(
self.domain_6, _ = Domain.objects.get_or_create(name="bdomain6.gov", state=Domain.State.DELETED, deleted_at=timezone.make_aware(datetime(1980, 10, 16))) name="bdomain5.gov", state=Domain.State.DELETED, deleted_at=timezone.make_aware(datetime(2023, 11, 1))
self.domain_7, _ = Domain.objects.get_or_create(name="xdomain7.gov", state=Domain.State.DELETED, deleted_at=timezone.now()) )
self.domain_8, _ = Domain.objects.get_or_create(name="sdomain8.gov", state=Domain.State.DELETED, deleted_at=timezone.now()) self.domain_6, _ = Domain.objects.get_or_create(
# 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()). name="bdomain6.gov", state=Domain.State.DELETED, deleted_at=timezone.make_aware(datetime(1980, 10, 16))
)
self.domain_7, _ = Domain.objects.get_or_create(
name="xdomain7.gov", state=Domain.State.DELETED, deleted_at=timezone.now()
)
self.domain_8, _ = Domain.objects.get_or_create(
name="sdomain8.gov", state=Domain.State.DELETED, deleted_at=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 # Deleted yesterday
self.domain_9, _ = Domain.objects.get_or_create(name="zdomain9.gov", state=Domain.State.DELETED, deleted_at=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time()))) self.domain_9, _ = Domain.objects.get_or_create(
name="zdomain9.gov",
state=Domain.State.DELETED,
deleted_at=timezone.make_aware(datetime.combine(date.today() - timedelta(days=1), datetime.min.time())),
)
# ready tomorrow # ready tomorrow
self.domain_10, _ = Domain.objects.get_or_create(name="adomain10.gov", state=Domain.State.READY, ready_at=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time()))) self.domain_10, _ = Domain.objects.get_or_create(
name="adomain10.gov",
state=Domain.State.READY,
ready_at=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())),
)
self.domain_information_1, _ = DomainInformation.objects.get_or_create( self.domain_information_1, _ = DomainInformation.objects.get_or_create(
creator=self.user, creator=self.user,
domain=self.domain_1, domain=self.domain_1,
@ -423,24 +447,25 @@ class ExportDataTest(TestCase):
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_domains_to_writer_with_date_filter_pulls_domains_in_range(self): def test_export_domains_to_writer_with_date_filter_pulls_domains_in_range(self):
"""Test that domains that are """Test that domains that are
1. READY and their ready_at dates are in range 1. READY and their ready_at dates are in range
2. DELETED and their deleted_at dates are in range 2. DELETED and their deleted_at 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 ready_at/deleted_at dates first, names second. Test that ready domains are sorted by ready_at/deleted_at dates first, names second.
We considered testing export_data_growth_to_csv which calls export_domains_to_writer We considered testing export_data_growth_to_csv which calls export_domains_to_writer
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 is created_at is not needed for the report."""
# 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()). # 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())) 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())) start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
@ -455,7 +480,10 @@ class ExportDataTest(TestCase):
"Status", "Status",
"Expiration date", "Expiration date",
] ]
sort_fields = ["created_at","domain__name",] sort_fields = [
"created_at",
"domain__name",
]
sort_fields_for_additional_domains = [ sort_fields_for_additional_domains = [
"domain__deleted_at", "domain__deleted_at",
"domain__name", "domain__name",
@ -476,15 +504,22 @@ class ExportDataTest(TestCase):
} }
# Call the export function # Call the export function
export_domains_to_writer(writer, columns, sort_fields, filter_condition, sort_fields_for_additional_domains, filter_conditions_for_additional_domains) export_domains_to_writer(
writer,
columns,
sort_fields,
filter_condition,
sort_fields_for_additional_domains,
filter_conditions_for_additional_domains,
)
# Reset the CSV file's position to the beginning # Reset the CSV file's position to the beginning
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name # 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_at then name # and DELETED domains deleted between today-2 and today+2, sorted by deleted_at then name
expected_content = ( expected_content = (
"Domain name,Domain type,Agency,Organization name,City," "Domain name,Domain type,Agency,Organization name,City,"
@ -500,12 +535,13 @@ class ExportDataTest(TestCase):
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
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)
class HelperFunctions(TestCase): class HelperFunctions(TestCase):
"""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):
expected_date = timezone.make_aware(datetime(2023, 11, 1)) expected_date = timezone.make_aware(datetime(2023, 11, 1))
actual_date = get_default_start_date() actual_date = get_default_start_date()
@ -515,4 +551,4 @@ class HelperFunctions(TestCase):
# Note: You may need to mock timezone.now() for accurate testing # Note: You may need to mock timezone.now() for accurate testing
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())

View file

@ -1,6 +1,6 @@
import csv import csv
import logging import logging
from datetime import date, datetime from datetime import datetime
from registrar.models.domain import Domain from registrar.models.domain import Domain
from registrar.models.domain_information import DomainInformation from registrar.models.domain_information import DomainInformation
from registrar.models.public_contact import PublicContact from registrar.models.public_contact import PublicContact
@ -11,10 +11,12 @@ from django.utils import timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_domain_infos(filter_condition, sort_fields): def get_domain_infos(filter_condition, sort_fields):
domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields)
return domain_infos return domain_infos
def write_row(writer, columns, domain_info): def write_row(writer, columns, domain_info):
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
# For linter # For linter
@ -44,34 +46,48 @@ def write_row(writer, columns, domain_info):
} }
writer.writerow([FIELDS.get(column, "") for column in columns]) writer.writerow([FIELDS.get(column, "") for column in columns])
def export_domains_to_writer(writer, columns, sort_fields, filter_condition, sort_fields_for_additional_domains=None, filter_condition_for_additional_domains=None):
def export_domains_to_writer(
writer,
columns,
sort_fields,
filter_condition,
sort_fields_for_additional_domains=None,
filter_condition_for_additional_domains=None,
):
""" """
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 fltered and sorted domains.
The 'additional' params enable us to concatenate 2 different filtered lists. The 'additional' params enable us to concatenate 2 different filtered lists.
""" """
# write columns headers to writer # write columns headers to writer
writer.writerow(columns) writer.writerow(columns)
# Get the domainInfos # Get the domainInfos
domainInfos = get_domain_infos(filter_condition, sort_fields) domainInfos = get_domain_infos(filter_condition, sort_fields)
# Condition is true for export_data_growth_to_csv. This is an OR situation so we can' combine the filters # Condition is true for export_data_growth_to_csv. This is an OR situation so we can' combine the filters
# in one query. # in one query.
if filter_condition_for_additional_domains is not None and 'domain__deleted_at__lt' in filter_condition_for_additional_domains: if (
filter_condition_for_additional_domains is not None
and "domain__deleted_at__lt" in filter_condition_for_additional_domains
):
# Get the deleted domain infos # Get the deleted domain infos
deleted_domainInfos = get_domain_infos(filter_condition_for_additional_domains, sort_fields_for_additional_domains) deleted_domainInfos = get_domain_infos(
filter_condition_for_additional_domains, sort_fields_for_additional_domains
)
# Combine the two querysets into a single iterable # Combine the two querysets into a single iterable
all_domainInfos = list(chain(domainInfos, deleted_domainInfos)) all_domainInfos = list(chain(domainInfos, deleted_domainInfos))
else: else:
all_domainInfos = list(domainInfos) all_domainInfos = list(domainInfos)
# Write rows to CSV # Write rows to CSV
for domain_info in all_domainInfos: for domain_info in all_domainInfos:
write_row(writer, columns, domain_info) write_row(writer, columns, domain_info)
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"""
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# define columns to include in export # define columns to include in export
columns = [ columns = [
@ -103,9 +119,10 @@ def export_data_type_to_csv(csv_file):
} }
export_domains_to_writer(writer, columns, sort_fields, filter_condition) export_domains_to_writer(writer, columns, sort_fields, filter_condition)
def export_data_full_to_csv(csv_file): def export_data_full_to_csv(csv_file):
"""All domains report""" """All domains report"""
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# define columns to include in export # define columns to include in export
columns = [ columns = [
@ -136,7 +153,7 @@ def export_data_full_to_csv(csv_file):
def export_data_federal_to_csv(csv_file): def export_data_federal_to_csv(csv_file):
"""Federal domains report""" """Federal domains report"""
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# define columns to include in export # define columns to include in export
columns = [ columns = [
@ -165,14 +182,17 @@ def export_data_federal_to_csv(csv_file):
} }
export_domains_to_writer(writer, columns, sort_fields, filter_condition) export_domains_to_writer(writer, columns, sort_fields, filter_condition)
def get_default_start_date(): def get_default_start_date():
# Default to a date that's prior to our first deployment # Default to a date that's prior to our first deployment
return timezone.make_aware(datetime(2023, 11, 1)) return timezone.make_aware(datetime(2023, 11, 1))
def get_default_end_date(): def get_default_end_date():
# Default to now() # Default to now()
return timezone.now() return timezone.now()
def export_data_growth_to_csv(csv_file, start_date, end_date): def export_data_growth_to_csv(csv_file, start_date, end_date):
""" """
Growth report: Growth report:
@ -181,21 +201,17 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
the start and end dates, as well as DELETED domains that are deleted between the start and end dates, as well as DELETED domains that are deleted between
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 = (
timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) timezone.make_aware(datetime.strptime(start_date, "%Y-%m-%d")) if start_date else get_default_start_date()
if start_date
else get_default_start_date()
) )
end_date_formatted = ( end_date_formatted = (
timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) timezone.make_aware(datetime.strptime(end_date, "%Y-%m-%d")) if end_date else get_default_end_date()
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",
@ -219,7 +235,7 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
"domain__ready_at__lt": end_date_formatted, "domain__ready_at__lt": end_date_formatted,
"domain__ready_at__gt": start_date_formatted, "domain__ready_at__gt": start_date_formatted,
} }
# We also want domains deleted between sar and end dates, sorted # We also want domains deleted between sar and end dates, sorted
sort_fields_for_additional_domains = [ sort_fields_for_additional_domains = [
"domain__deleted_at", "domain__deleted_at",
@ -230,5 +246,12 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
"domain__created_at__lt": end_date_formatted, "domain__created_at__lt": end_date_formatted,
"domain__created_at__gt": start_date_formatted, "domain__created_at__gt": start_date_formatted,
} }
export_domains_to_writer(writer, columns, sort_fields, filter_condition, sort_fields_for_additional_domains, filter_condition_for_additional_domains) export_domains_to_writer(
writer,
columns,
sort_fields,
filter_condition,
sort_fields_for_additional_domains,
filter_condition_for_additional_domains,
)

View file

@ -2,8 +2,6 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.views import View 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 registrar.utility import csv_export
@ -13,19 +11,16 @@ logger = logging.getLogger(__name__)
class ExportData(View): class ExportData(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
start_date = request.GET.get('start_date', '') start_date = request.GET.get("start_date", "")
end_date = request.GET.get('end_date', '') end_date = request.GET.get("end_date", "")
response = HttpResponse(content_type="text/csv") response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="growth-from-{start_date}-to-{end_date}.csv"' response["Content-Disposition"] = f'attachment; filename="growth-from-{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_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_growth_to_csv(response, start_date, end_date)
return response
return response