mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 02:19:23 +02:00
Merge remote-tracking branch 'origin/main' into nl/2248-add-org-and-portfolio-table
This commit is contained in:
commit
d7de91b7f9
21 changed files with 876 additions and 208 deletions
|
@ -1500,6 +1500,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"authorizing_official",
|
"authorizing_official",
|
||||||
"other_contacts",
|
"other_contacts",
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1575,6 +1577,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
|
|
|
@ -773,3 +773,12 @@ div.dja__model-description{
|
||||||
.module caption, .inline-group h2 {
|
.module caption, .inline-group h2 {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapped-button-group {
|
||||||
|
// This button group has too many items
|
||||||
|
flex-wrap: wrap;
|
||||||
|
// Fix a weird spacing issue with USWDS a buttons in DJA
|
||||||
|
a.button {
|
||||||
|
padding: 6px 8px 10px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ from registrar.views.admin_views import (
|
||||||
ExportDataType,
|
ExportDataType,
|
||||||
ExportDataUnmanagedDomains,
|
ExportDataUnmanagedDomains,
|
||||||
AnalyticsView,
|
AnalyticsView,
|
||||||
|
ExportDomainRequestDataFull,
|
||||||
)
|
)
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step
|
||||||
|
@ -66,6 +67,11 @@ urlpatterns = [
|
||||||
ExportDataType.as_view(),
|
ExportDataType.as_view(),
|
||||||
name="export_data_type",
|
name="export_data_type",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"admin/analytics/export_data_domain_requests_full/",
|
||||||
|
ExportDomainRequestDataFull.as_view(),
|
||||||
|
name="export_data_domain_requests_full",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"admin/analytics/export_data_full/",
|
"admin/analytics/export_data_full/",
|
||||||
ExportDataFull.as_view(),
|
ExportDataFull.as_view(),
|
||||||
|
|
|
@ -648,20 +648,27 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
|
||||||
|
|
||||||
|
|
||||||
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
class CisaRepresentativeForm(BaseDeletableRegistrarForm):
|
||||||
|
cisa_representative_first_name = forms.CharField(
|
||||||
|
label="First name / given name",
|
||||||
|
error_messages={"required": "Enter the first name / given name of the CISA regional representative."},
|
||||||
|
)
|
||||||
|
cisa_representative_last_name = forms.CharField(
|
||||||
|
label="Last name / family name",
|
||||||
|
error_messages={"required": "Enter the last name / family name of the CISA regional representative."},
|
||||||
|
)
|
||||||
cisa_representative_email = forms.EmailField(
|
cisa_representative_email = forms.EmailField(
|
||||||
required=True,
|
label="Your representative’s email (optional)",
|
||||||
max_length=None,
|
max_length=None,
|
||||||
label="Your representative’s email",
|
required=False,
|
||||||
|
error_messages={
|
||||||
|
"invalid": ("Enter your representative’s email address in the required format, like name@example.com."),
|
||||||
|
},
|
||||||
validators=[
|
validators=[
|
||||||
MaxLengthValidator(
|
MaxLengthValidator(
|
||||||
320,
|
320,
|
||||||
message="Response must be less than 320 characters.",
|
message="Response must be less than 320 characters.",
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
error_messages={
|
|
||||||
"invalid": ("Enter your email address in the required format, like name@example.com."),
|
|
||||||
"required": ("Enter the email address of your CISA regional representative."),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-06-12 20:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0100_domainrequest_action_needed_reason"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="cisa_representative_first_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="cisa_representative_last_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="has_anything_else_text",
|
||||||
|
field=models.BooleanField(
|
||||||
|
blank=True, help_text="Determines if the user has a anything_else or not", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="has_cisa_representative",
|
||||||
|
field=models.BooleanField(
|
||||||
|
blank=True, help_text="Determines if the user has a representative email or not", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="cisa_representative_first_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, null=True, verbose_name="CISA regional representative first name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="cisa_representative_last_name",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, db_index=True, null=True, verbose_name="CISA regional representative last name"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domaininformation",
|
||||||
|
name="cisa_representative_email",
|
||||||
|
field=models.EmailField(
|
||||||
|
blank=True, max_length=320, null=True, verbose_name="CISA regional representative email"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainrequest",
|
||||||
|
name="cisa_representative_email",
|
||||||
|
field=models.EmailField(
|
||||||
|
blank=True, max_length=320, null=True, verbose_name="CISA regional representative email"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -225,13 +225,45 @@ class DomainInformation(TimeStampedModel):
|
||||||
verbose_name="Additional details",
|
verbose_name="Additional details",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This is a drop-in replacement for a has_anything_else_text() function.
|
||||||
|
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||||
|
# a tertiary state. We should not display this in /admin.
|
||||||
|
has_anything_else_text = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Determines if the user has a anything_else or not",
|
||||||
|
)
|
||||||
|
|
||||||
cisa_representative_email = models.EmailField(
|
cisa_representative_email = models.EmailField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="CISA regional representative",
|
verbose_name="CISA regional representative email",
|
||||||
max_length=320,
|
max_length=320,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cisa_representative_first_name = models.CharField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative first name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
cisa_representative_last_name = models.CharField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative last name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# This is a drop-in replacement for an has_cisa_representative() function.
|
||||||
|
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||||
|
# a tertiary state. We should not display this in /admin.
|
||||||
|
has_cisa_representative = models.BooleanField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Determines if the user has a representative email or not",
|
||||||
|
)
|
||||||
|
|
||||||
is_policy_acknowledged = models.BooleanField(
|
is_policy_acknowledged = models.BooleanField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -252,6 +284,30 @@ class DomainInformation(TimeStampedModel):
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def sync_yes_no_form_fields(self):
|
||||||
|
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||||
|
We handle that here for def save().
|
||||||
|
"""
|
||||||
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
|
if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None:
|
||||||
|
self.has_cisa_representative = (
|
||||||
|
self.cisa_representative_first_name != "" and self.cisa_representative_last_name != ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# This check is required to ensure that the form doesn't start out checked
|
||||||
|
if self.has_cisa_representative is not None:
|
||||||
|
self.has_cisa_representative = (
|
||||||
|
self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None
|
||||||
|
) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None)
|
||||||
|
|
||||||
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
|
if self.anything_else is not None:
|
||||||
|
self.has_anything_else_text = self.anything_else != ""
|
||||||
|
|
||||||
|
# This check is required to ensure that the form doesn't start out checked.
|
||||||
|
if self.has_anything_else_text is not None:
|
||||||
|
self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None
|
||||||
|
|
||||||
def sync_organization_type(self):
|
def sync_organization_type(self):
|
||||||
"""
|
"""
|
||||||
Updates the organization_type (without saving) to match
|
Updates the organization_type (without saving) to match
|
||||||
|
@ -286,6 +342,7 @@ class DomainInformation(TimeStampedModel):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Save override for custom properties"""
|
"""Save override for custom properties"""
|
||||||
|
self.sync_yes_no_form_fields()
|
||||||
self.sync_organization_type()
|
self.sync_organization_type()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,11 @@ class DomainRequest(TimeStampedModel):
|
||||||
WITHDRAWN = "withdrawn", "Withdrawn"
|
WITHDRAWN = "withdrawn", "Withdrawn"
|
||||||
STARTED = "started", "Started"
|
STARTED = "started", "Started"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_status_label(cls, status_name: str):
|
||||||
|
"""Returns the associated label for a given status name"""
|
||||||
|
return cls(status_name).label if status_name else None
|
||||||
|
|
||||||
class StateTerritoryChoices(models.TextChoices):
|
class StateTerritoryChoices(models.TextChoices):
|
||||||
ALABAMA = "AL", "Alabama (AL)"
|
ALABAMA = "AL", "Alabama (AL)"
|
||||||
ALASKA = "AK", "Alaska (AK)"
|
ALASKA = "AK", "Alaska (AK)"
|
||||||
|
@ -133,6 +138,14 @@ class DomainRequest(TimeStampedModel):
|
||||||
SPECIAL_DISTRICT = "special_district", "Special district"
|
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||||
SCHOOL_DISTRICT = "school_district", "School district"
|
SCHOOL_DISTRICT = "school_district", "School district"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_org_label(cls, org_name: str):
|
||||||
|
"""Returns the associated label for a given org name"""
|
||||||
|
org_names = org_name.split("_election")
|
||||||
|
if len(org_names) > 0:
|
||||||
|
org_name = org_names[0]
|
||||||
|
return cls(org_name).label if org_name else None
|
||||||
|
|
||||||
class OrgChoicesElectionOffice(models.TextChoices):
|
class OrgChoicesElectionOffice(models.TextChoices):
|
||||||
"""
|
"""
|
||||||
Primary organization choices for Django admin:
|
Primary organization choices for Django admin:
|
||||||
|
@ -487,10 +500,24 @@ class DomainRequest(TimeStampedModel):
|
||||||
cisa_representative_email = models.EmailField(
|
cisa_representative_email = models.EmailField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name="CISA regional representative",
|
verbose_name="CISA regional representative email",
|
||||||
max_length=320,
|
max_length=320,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cisa_representative_first_name = models.CharField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative first name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
cisa_representative_last_name = models.CharField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="CISA regional representative last name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
# This is a drop-in replacement for an has_cisa_representative() function.
|
# This is a drop-in replacement for an has_cisa_representative() function.
|
||||||
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
# In order to track if the user has clicked the yes/no field (while keeping a none default), we need
|
||||||
# a tertiary state. We should not display this in /admin.
|
# a tertiary state. We should not display this in /admin.
|
||||||
|
@ -587,16 +614,17 @@ class DomainRequest(TimeStampedModel):
|
||||||
"""Some yes/no forms use a db field to track whether it was checked or not.
|
"""Some yes/no forms use a db field to track whether it was checked or not.
|
||||||
We handle that here for def save().
|
We handle that here for def save().
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# This ensures that if we have prefilled data, the form is prepopulated
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
if self.cisa_representative_email is not None:
|
if self.cisa_representative_first_name is not None or self.cisa_representative_last_name is not None:
|
||||||
self.has_cisa_representative = self.cisa_representative_email != ""
|
self.has_cisa_representative = (
|
||||||
|
self.cisa_representative_first_name != "" and self.cisa_representative_last_name != ""
|
||||||
|
)
|
||||||
|
|
||||||
# This check is required to ensure that the form doesn't start out checked
|
# This check is required to ensure that the form doesn't start out checked
|
||||||
if self.has_cisa_representative is not None:
|
if self.has_cisa_representative is not None:
|
||||||
self.has_cisa_representative = (
|
self.has_cisa_representative = (
|
||||||
self.cisa_representative_email != "" and self.cisa_representative_email is not None
|
self.cisa_representative_first_name != "" and self.cisa_representative_first_name is not None
|
||||||
)
|
) and (self.cisa_representative_last_name != "" and self.cisa_representative_last_name is not None)
|
||||||
|
|
||||||
# This ensures that if we have prefilled data, the form is prepopulated
|
# This ensures that if we have prefilled data, the form is prepopulated
|
||||||
if self.anything_else is not None:
|
if self.anything_else is not None:
|
||||||
|
@ -994,11 +1022,12 @@ class DomainRequest(TimeStampedModel):
|
||||||
def has_additional_details(self) -> bool:
|
def has_additional_details(self) -> bool:
|
||||||
"""Combines the has_anything_else_text and has_cisa_representative fields,
|
"""Combines the has_anything_else_text and has_cisa_representative fields,
|
||||||
then returns if this domain request has either of them."""
|
then returns if this domain request has either of them."""
|
||||||
|
|
||||||
# Split out for linter
|
# Split out for linter
|
||||||
has_details = False
|
|
||||||
if self.has_anything_else_text or self.has_cisa_representative:
|
|
||||||
has_details = True
|
has_details = True
|
||||||
|
|
||||||
|
if self.has_anything_else_text is None or self.has_cisa_representative is None:
|
||||||
|
has_details = False
|
||||||
return has_details
|
return has_details
|
||||||
|
|
||||||
def is_federal(self) -> Union[bool, None]:
|
def is_federal(self) -> Union[bool, None]:
|
||||||
|
@ -1107,14 +1136,19 @@ class DomainRequest(TimeStampedModel):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _cisa_rep_and_email_check(self):
|
def _cisa_rep_check(self):
|
||||||
# Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep
|
# Either does not have a CISA rep, OR has a CISA rep + both first name
|
||||||
return (
|
# and last name are NOT empty and are NOT an empty string
|
||||||
|
to_return = (
|
||||||
self.has_cisa_representative is True
|
self.has_cisa_representative is True
|
||||||
and self.cisa_representative_email is not None
|
and self.cisa_representative_first_name is not None
|
||||||
and self.cisa_representative_email != ""
|
and self.cisa_representative_first_name != ""
|
||||||
|
and self.cisa_representative_last_name is not None
|
||||||
|
and self.cisa_representative_last_name != ""
|
||||||
) or self.has_cisa_representative is False
|
) or self.has_cisa_representative is False
|
||||||
|
|
||||||
|
return to_return
|
||||||
|
|
||||||
def _anything_else_radio_button_and_text_field_check(self):
|
def _anything_else_radio_button_and_text_field_check(self):
|
||||||
# Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
|
# Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
|
||||||
return (
|
return (
|
||||||
|
@ -1122,7 +1156,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
) or self.has_anything_else_text is False
|
) or self.has_anything_else_text is False
|
||||||
|
|
||||||
def _is_additional_details_complete(self):
|
def _is_additional_details_complete(self):
|
||||||
return self._cisa_rep_and_email_check() and self._anything_else_radio_button_and_text_field_check()
|
return self._cisa_rep_check() and self._anything_else_radio_button_and_text_field_check()
|
||||||
|
|
||||||
def _is_policy_acknowledgement_complete(self):
|
def _is_policy_acknowledgement_complete(self):
|
||||||
return self.is_policy_acknowledged is not None
|
return self.is_policy_acknowledged is not None
|
||||||
|
|
|
@ -298,3 +298,26 @@ def replace_url_queryparams(url_to_modify: str, query_params, convert_list_to_cs
|
||||||
new_url = urlunparse(url_parts)
|
new_url = urlunparse(url_parts)
|
||||||
|
|
||||||
return new_url
|
return new_url
|
||||||
|
|
||||||
|
|
||||||
|
def convert_queryset_to_dict(queryset, is_model=True, key="id"):
|
||||||
|
"""
|
||||||
|
Transforms a queryset into a dictionary keyed by a specified key (like "id").
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
requests (QuerySet or list of dicts): Input data.
|
||||||
|
is_model (bool): Indicates if each item in 'queryset' are model instances (True) or dictionaries (False).
|
||||||
|
key (str): Key or attribute to use for the resulting dictionary's keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary with keys derived from 'key' and values corresponding to items in 'queryset'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if is_model:
|
||||||
|
request_dict = {getattr(value, key): value for value in queryset}
|
||||||
|
else:
|
||||||
|
# Querysets sometimes contain sets of dictionaries.
|
||||||
|
# Calling .values is an example of this.
|
||||||
|
request_dict = {value[key]: value for value in queryset}
|
||||||
|
|
||||||
|
return request_dict
|
||||||
|
|
|
@ -27,28 +27,35 @@
|
||||||
<div class="module height-full">
|
<div class="module height-full">
|
||||||
<h2>Current domains</h2>
|
<h2>Current domains</h2>
|
||||||
<div class="padding-top-2 padding-x-2">
|
<div class="padding-top-2 padding-x-2">
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group wrapped-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_type' %}" class="button" role="button">
|
<a href="{% url 'export_data_type' %}" class="button text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<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>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">All domain metadata</span>
|
</svg><span class="margin-left-05">All domain metadata</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_full' %}" class="button" role="button">
|
<a href="{% url 'export_data_full' %}" class="button text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<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>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Current full</span>
|
</svg><span class="margin-left-05">Current full</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_federal' %}" class="button" role="button">
|
<a href="{% url 'export_data_federal' %}" class="button text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<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>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Current federal</span>
|
</svg><span class="margin-left-05">Current federal</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<a href="{% url 'export_data_domain_requests_full' %}" class="button text-no-wrap" 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 requests metadata</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
{# commented out so it does not appear at this point on this page #}
|
{# commented out so it does not appear at this point on this page #}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<!-- TODO-NL: (refactor) Breakup into two separate components-->
|
|
||||||
{% block form_fields %}
|
{% block form_fields %}
|
||||||
<fieldset class="usa-fieldset margin-top-2">
|
<fieldset class="usa-fieldset margin-top-2">
|
||||||
<legend>
|
<legend>
|
||||||
|
@ -22,13 +21,13 @@
|
||||||
{% input_with_errors forms.0.has_cisa_representative %}
|
{% input_with_errors forms.0.has_cisa_representative %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
<!-- TODO-NL: Hookup forms.0 to yes/no form for cisa representative (backend def)-->
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div id="cisa-representative" class="cisa-representative-form">
|
<div id="cisa-representative" class="cisa-representative-form margin-top-3">
|
||||||
|
{% input_with_errors forms.1.cisa_representative_first_name %}
|
||||||
|
{% input_with_errors forms.1.cisa_representative_last_name %}
|
||||||
{% input_with_errors forms.1.cisa_representative_email %}
|
{% input_with_errors forms.1.cisa_representative_email %}
|
||||||
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
|
{# forms.1 is a form for inputting the e-mail of a cisa representative #}
|
||||||
<!-- TODO-NL: Hookup forms.1 to cisa representative form (backend def) -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +41,6 @@
|
||||||
{% input_with_errors forms.2.has_anything_else_text %}
|
{% input_with_errors forms.2.has_anything_else_text %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
|
||||||
<!-- TODO-NL: Hookup forms.2 to yes/no form for anything else form (backend def)-->
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div id="anything-else">
|
<div id="anything-else">
|
||||||
|
@ -50,6 +48,5 @@
|
||||||
{% input_with_errors forms.3.anything_else %}
|
{% input_with_errors forms.3.anything_else %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
|
||||||
<!-- TODO-NL: Hookup forms.3 to anything else form (backend def) -->
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -157,9 +157,20 @@
|
||||||
|
|
||||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
{% with title=form_titles|get_item:step %}
|
||||||
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
{% if domain_request.has_additional_details %}
|
||||||
{% endwith %}
|
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
|
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
|
{% if domain_request.cisa_representative_first_name %}
|
||||||
|
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||||
|
{% if domain_request.cisa_representative_email %}
|
||||||
|
<li>{{domain_request.cisa_representative_email}}</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
No
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
<h3 class="register-form-review-header">Anything else</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
|
@ -169,6 +180,10 @@
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,15 @@
|
||||||
|
|
||||||
{# We always show this field even if None #}
|
{# We always show this field even if None #}
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
{% include "includes/summary_item.html" with title='Additional details' sub_header_text='CISA regional representative' value=DomainRequest.cisa_representative_email custom_text_for_value_none='No' heading_level=heading_level %}
|
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||||
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
|
{% if domain_request.cisa_representative_first_name %}
|
||||||
|
{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}
|
||||||
|
{% else %}
|
||||||
|
No
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3 class="register-form-review-header">Anything else</h3>
|
<h3 class="register-form-review-header">Anything else</h3>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.anything_else %}
|
{% if DomainRequest.anything_else %}
|
||||||
|
@ -128,7 +136,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -735,19 +735,53 @@ class MockDb(TestCase):
|
||||||
self.domain_request_4 = completed_domain_request(
|
self.domain_request_4 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
name="city4.gov",
|
name="city4.gov",
|
||||||
|
is_election_board=True,
|
||||||
|
generic_org_type="city",
|
||||||
)
|
)
|
||||||
self.domain_request_5 = completed_domain_request(
|
self.domain_request_5 = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED,
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
name="city5.gov",
|
name="city5.gov",
|
||||||
)
|
)
|
||||||
|
self.domain_request_6 = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
|
name="city6.gov",
|
||||||
|
)
|
||||||
self.domain_request_3.submit()
|
self.domain_request_3.submit()
|
||||||
self.domain_request_4.submit()
|
self.domain_request_4.submit()
|
||||||
|
self.domain_request_6.submit()
|
||||||
|
|
||||||
|
other, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Testy1232",
|
||||||
|
last_name="Tester24",
|
||||||
|
title="Another Tester",
|
||||||
|
email="te2@town.com",
|
||||||
|
phone="(555) 555 5557",
|
||||||
|
)
|
||||||
|
other_2, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Meow",
|
||||||
|
last_name="Tester24",
|
||||||
|
title="Another Tester",
|
||||||
|
email="te2@town.com",
|
||||||
|
phone="(555) 555 5557",
|
||||||
|
)
|
||||||
|
website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||||
|
website_2, _ = Website.objects.get_or_create(website="cheeseville.gov")
|
||||||
|
website_3, _ = Website.objects.get_or_create(website="https://www.example.com")
|
||||||
|
website_4, _ = Website.objects.get_or_create(website="https://www.example2.com")
|
||||||
|
|
||||||
|
self.domain_request_3.other_contacts.add(other, other_2)
|
||||||
|
self.domain_request_3.alternative_domains.add(website, website_2)
|
||||||
|
self.domain_request_3.current_websites.add(website_3, website_4)
|
||||||
|
self.domain_request_3.cisa_representative_email = "test@igorville.com"
|
||||||
self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
self.domain_request_3.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
|
||||||
self.domain_request_3.save()
|
self.domain_request_3.save()
|
||||||
|
|
||||||
|
self.domain_request_4.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
self.domain_request_4.save()
|
self.domain_request_4.save()
|
||||||
|
|
||||||
|
self.domain_request_6.submission_date = get_time_aware_date(datetime(2024, 4, 2))
|
||||||
|
self.domain_request_6.save()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
PublicContact.objects.all().delete()
|
PublicContact.objects.all().delete()
|
||||||
|
@ -808,12 +842,13 @@ def create_ready_domain():
|
||||||
|
|
||||||
|
|
||||||
# TODO in 1793: Remove the federal agency/updated federal agency fields
|
# TODO in 1793: Remove the federal agency/updated federal agency fields
|
||||||
def completed_domain_request(
|
def completed_domain_request( # noqa
|
||||||
has_other_contacts=True,
|
has_other_contacts=True,
|
||||||
has_current_website=True,
|
has_current_website=True,
|
||||||
has_alternative_gov_domain=True,
|
has_alternative_gov_domain=True,
|
||||||
has_about_your_organization=True,
|
has_about_your_organization=True,
|
||||||
has_anything_else=True,
|
has_anything_else=True,
|
||||||
|
has_cisa_representative=True,
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
user=False,
|
user=False,
|
||||||
submitter=False,
|
submitter=False,
|
||||||
|
@ -895,6 +930,10 @@ def completed_domain_request(
|
||||||
domain_request.current_websites.add(current)
|
domain_request.current_websites.add(current)
|
||||||
if has_alternative_gov_domain:
|
if has_alternative_gov_domain:
|
||||||
domain_request.alternative_domains.add(alt)
|
domain_request.alternative_domains.add(alt)
|
||||||
|
if has_cisa_representative:
|
||||||
|
domain_request.cisa_representative_first_name = "CISA-first-name"
|
||||||
|
domain_request.cisa_representative_last_name = "CISA-last-name"
|
||||||
|
domain_request.cisa_representative_email = "cisaRep@igorville.gov"
|
||||||
|
|
||||||
return domain_request
|
return domain_request
|
||||||
|
|
||||||
|
|
|
@ -2326,6 +2326,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"has_anything_else_text",
|
"has_anything_else_text",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
"has_cisa_representative",
|
"has_cisa_representative",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
"submission_date",
|
"submission_date",
|
||||||
|
@ -2358,6 +2360,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"no_other_contacts_rationale",
|
"no_other_contacts_rationale",
|
||||||
"anything_else",
|
"anything_else",
|
||||||
"is_policy_acknowledged",
|
"is_policy_acknowledged",
|
||||||
|
"cisa_representative_first_name",
|
||||||
|
"cisa_representative_last_name",
|
||||||
"cisa_representative_email",
|
"cisa_representative_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1802,93 +1802,129 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
def test_is_additional_details_complete(self):
|
def test_is_additional_details_complete(self):
|
||||||
test_cases = [
|
test_cases = [
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
|
# Firstname - Yes
|
||||||
|
# Lastname - Yes
|
||||||
# Email - Yes
|
# Email - Yes
|
||||||
# Anything Else Radio - Yes
|
# Anything Else Radio - Yes
|
||||||
# Anything Else Text - Yes
|
# Anything Else Text - Yes
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_first_name": "cisa-first-name",
|
||||||
|
"cisa_representative_last_name": "cisa-last-name",
|
||||||
"cisa_representative_email": "some@cisarepemail.com",
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": "Some text",
|
"anything_else": "Some text",
|
||||||
"expected": True,
|
"expected": True,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
|
# Firstname - Yes
|
||||||
|
# Lastname - Yes
|
||||||
# Email - Yes
|
# Email - Yes
|
||||||
# Anything Else Radio - Yes
|
# Anything Else Radio - Yes
|
||||||
# Anything Else Text - None
|
# Anything Else Text - None
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_first_name": "cisa-first-name",
|
||||||
|
"cisa_representative_last_name": "cisa-last-name",
|
||||||
"cisa_representative_email": "some@cisarepemail.com",
|
"cisa_representative_email": "some@cisarepemail.com",
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
"expected": True,
|
"expected": True,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
# Email - Yes
|
# Firstname - Yes
|
||||||
|
# Lastname - Yes
|
||||||
|
# Email - None >> e-mail is optional so it should not change anything setting this to None
|
||||||
# Anything Else Radio - No
|
# Anything Else Radio - No
|
||||||
# Anything Else Text - No
|
# Anything Else Text - No
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
"cisa_representative_email": "some@cisarepemail.com",
|
"cisa_representative_first_name": "cisa-first-name",
|
||||||
|
"cisa_representative_last_name": "cisa-last-name",
|
||||||
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": False,
|
"has_anything_else_text": False,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
"expected": True,
|
"expected": True,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
# Email - Yes
|
# Firstname - Yes
|
||||||
# Anything Else Radio - None
|
# Lastname - Yes
|
||||||
# Anything Else Text - None
|
|
||||||
{
|
|
||||||
"has_cisa_representative": True,
|
|
||||||
"cisa_representative_email": "some@cisarepemail.com",
|
|
||||||
"has_anything_else_text": None,
|
|
||||||
"anything_else": None,
|
|
||||||
"expected": False,
|
|
||||||
},
|
|
||||||
# CISA Rep - Yes
|
|
||||||
# Email - None
|
# Email - None
|
||||||
# Anything Else Radio - None
|
# Anything Else Radio - None
|
||||||
# Anything Else Text - None
|
# Anything Else Text - None
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_first_name": "cisa-first-name",
|
||||||
|
"cisa_representative_last_name": "cisa-last-name",
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": None,
|
"has_anything_else_text": None,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
"expected": False,
|
"expected": False,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
|
# Firstname - None
|
||||||
|
# Lastname - None
|
||||||
|
# Email - None
|
||||||
|
# Anything Else Radio - None
|
||||||
|
# Anything Else Text - None
|
||||||
|
{
|
||||||
|
"has_cisa_representative": True,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
|
"cisa_representative_email": None,
|
||||||
|
"has_anything_else_text": None,
|
||||||
|
"anything_else": None,
|
||||||
|
"expected": False,
|
||||||
|
},
|
||||||
|
# CISA Rep - Yes
|
||||||
|
# Firstname - None
|
||||||
|
# Lastname - None
|
||||||
# Email - None
|
# Email - None
|
||||||
# Anything Else Radio - No
|
# Anything Else Radio - No
|
||||||
# Anything Else Text - No
|
# Anything Else Text - No
|
||||||
# sync_yes_no will override has_cisa_representative to be False if cisa_representative_email is None
|
# sync_yes_no will override has_cisa_representative to be False if cisa_representative_first_name is None
|
||||||
# therefore, our expected will be True
|
# therefore, our expected will be True
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
# Above will be overridden to False if cisa_representative_first_name is None
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": False,
|
"has_anything_else_text": False,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
"expected": True,
|
"expected": True,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
|
# Firstname - None
|
||||||
|
# Lastname - None
|
||||||
# Email - None
|
# Email - None
|
||||||
# Anything Else Radio - Yes
|
# Anything Else Radio - Yes
|
||||||
# Anything Else Text - None
|
# Anything Else Text - None
|
||||||
|
# NOTE: We should never have an instance where only firstname or only lastname are populated
|
||||||
|
# (they are both required)
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
# Above will be overridden to False if cisa_representative_first_name is None or
|
||||||
|
# cisa_representative_last_name is None bc of sync_yes_no_form_fields
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
"expected": True,
|
"expected": True,
|
||||||
},
|
},
|
||||||
# CISA Rep - Yes
|
# CISA Rep - Yes
|
||||||
|
# Firstname - None
|
||||||
|
# Lastname - None
|
||||||
# Email - None
|
# Email - None
|
||||||
# Anything Else Radio - Yes
|
# Anything Else Radio - Yes
|
||||||
# Anything Else Text - Yes
|
# Anything Else Text - Yes
|
||||||
{
|
{
|
||||||
"has_cisa_representative": True,
|
"has_cisa_representative": True,
|
||||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
# Above will be overridden to False if cisa_representative_first_name is None or
|
||||||
|
# cisa_representative_last_name is None bc of sync_yes_no_form_fields
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": "Some text",
|
"anything_else": "Some text",
|
||||||
|
@ -1899,6 +1935,8 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
# Anything Else Text - Yes
|
# Anything Else Text - Yes
|
||||||
{
|
{
|
||||||
"has_cisa_representative": False,
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": "Some text",
|
"anything_else": "Some text",
|
||||||
|
@ -1909,6 +1947,8 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
# Anything Else Text - None
|
# Anything Else Text - None
|
||||||
{
|
{
|
||||||
"has_cisa_representative": False,
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": True,
|
"has_anything_else_text": True,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
|
@ -1919,6 +1959,8 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
# Anything Else Text - None
|
# Anything Else Text - None
|
||||||
{
|
{
|
||||||
"has_cisa_representative": False,
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": None,
|
"has_anything_else_text": None,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
|
@ -1930,6 +1972,8 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
# Anything Else Text - No
|
# Anything Else Text - No
|
||||||
{
|
{
|
||||||
"has_cisa_representative": False,
|
"has_cisa_representative": False,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": False,
|
"has_anything_else_text": False,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
|
@ -1939,6 +1983,8 @@ class TestDomainRequestIncomplete(TestCase):
|
||||||
# Anything Else Radio - None
|
# Anything Else Radio - None
|
||||||
{
|
{
|
||||||
"has_cisa_representative": None,
|
"has_cisa_representative": None,
|
||||||
|
"cisa_representative_first_name": None,
|
||||||
|
"cisa_representative_last_name": None,
|
||||||
"cisa_representative_email": None,
|
"cisa_representative_email": None,
|
||||||
"has_anything_else_text": None,
|
"has_anything_else_text": None,
|
||||||
"anything_else": None,
|
"anything_else": None,
|
||||||
|
|
|
@ -4,6 +4,7 @@ from django.test import Client, RequestFactory
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from registrar.models.domain_request import DomainRequest
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
export_data_managed_domains_to_csv,
|
export_data_managed_domains_to_csv,
|
||||||
export_data_unmanaged_domains_to_csv,
|
export_data_unmanaged_domains_to_csv,
|
||||||
|
@ -12,7 +13,7 @@ from registrar.utility.csv_export import (
|
||||||
write_csv_for_domains,
|
write_csv_for_domains,
|
||||||
get_default_start_date,
|
get_default_start_date,
|
||||||
get_default_end_date,
|
get_default_end_date,
|
||||||
write_csv_for_requests,
|
DomainRequestExport,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
@ -23,6 +24,7 @@ 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 django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from api.tests.common import less_console_noise_decorator
|
||||||
from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date
|
from .common import MockDb, MockEppLib, less_console_noise, get_time_aware_date
|
||||||
|
|
||||||
|
|
||||||
|
@ -667,10 +669,7 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
# Define columns, sort fields, and filter condition
|
# Define columns, sort fields, and filter condition
|
||||||
# We'll skip submission date because it's dynamic and therefore
|
# We'll skip submission date because it's dynamic and therefore
|
||||||
# impossible to set in expected_content
|
# impossible to set in expected_content
|
||||||
columns = [
|
columns = ["Domain request", "Domain type", "Federal type"]
|
||||||
"Requested domain",
|
|
||||||
"Organization type",
|
|
||||||
]
|
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"requested_domain__name",
|
"requested_domain__name",
|
||||||
]
|
]
|
||||||
|
@ -679,7 +678,12 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
"submission_date__lte": self.end_date,
|
"submission_date__lte": self.end_date,
|
||||||
"submission_date__gte": self.start_date,
|
"submission_date__gte": self.start_date,
|
||||||
}
|
}
|
||||||
write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
|
||||||
|
additional_values = ["requested_domain__name"]
|
||||||
|
all_requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
||||||
|
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(all_requests, {}, additional_values)
|
||||||
|
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||||
|
DomainRequestExport.write_csv_for_requests(writer, columns, requests_dict)
|
||||||
# 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
|
||||||
|
@ -687,9 +691,76 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
# 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 then name
|
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Requested domain,Organization type\n"
|
"Domain request,Domain type,Federal type\n"
|
||||||
"city3.gov,Federal - Executive\n"
|
"city3.gov,Federal,Executive\n"
|
||||||
"city4.gov,Federal - Executive\n"
|
"city4.gov,City,Executive\n"
|
||||||
|
"city6.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)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_full_domain_request_report(self):
|
||||||
|
"""Tests the full domain request report."""
|
||||||
|
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
|
# Call the report. Get existing fields from the report itself.
|
||||||
|
annotations = DomainRequestExport._full_domain_request_annotations()
|
||||||
|
additional_values = [
|
||||||
|
"requested_domain__name",
|
||||||
|
"federal_agency__agency",
|
||||||
|
"authorizing_official__first_name",
|
||||||
|
"authorizing_official__last_name",
|
||||||
|
"authorizing_official__email",
|
||||||
|
"authorizing_official__title",
|
||||||
|
"creator__first_name",
|
||||||
|
"creator__last_name",
|
||||||
|
"creator__email",
|
||||||
|
"investigator__email",
|
||||||
|
]
|
||||||
|
requests = DomainRequest.objects.exclude(status=DomainRequest.DomainRequestStatus.STARTED)
|
||||||
|
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(requests, annotations, additional_values)
|
||||||
|
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||||
|
DomainRequestExport.write_csv_for_requests(writer, DomainRequestExport.all_columns, requests_dict)
|
||||||
|
|
||||||
|
# Reset the CSV file's position to the beginning
|
||||||
|
csv_file.seek(0)
|
||||||
|
# Read the content into a variable
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
print(csv_content)
|
||||||
|
self.maxDiff = None
|
||||||
|
expected_content = (
|
||||||
|
# Header
|
||||||
|
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
||||||
|
"Federal agency,Organization name,Election office,City,State/territory,"
|
||||||
|
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
||||||
|
"Creator active requests count,Alternative domains,AO first name,AO last name,AO email,"
|
||||||
|
"AO title/role,Request purpose,Request additional details,Other contacts,"
|
||||||
|
"CISA regional representative,Current websites,Investigator\n"
|
||||||
|
# Content
|
||||||
|
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
|
"city3.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"
|
||||||
|
"cheeseville.gov | city1.gov | igorville.gov,Testy,Tester,testy@town.com,Chief Tester,"
|
||||||
|
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Meow Tester24 te2@town.com | "
|
||||||
|
"Testy1232 Tester24 te2@town.com | Testy Tester testy2@town.com,test@igorville.com,"
|
||||||
|
"city.com | https://www.example2.com | https://www.example.com,\n"
|
||||||
|
"city4.gov,2024-04-02,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||||
|
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
|
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n"
|
||||||
|
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
|
"city6.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||||
|
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
|
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
@ -741,5 +812,5 @@ class HelperFunctions(MockDb):
|
||||||
"submission_date__lte": self.end_date,
|
"submission_date__lte": self.end_date,
|
||||||
}
|
}
|
||||||
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
||||||
expected_content = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]
|
expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1]
|
||||||
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
||||||
|
|
|
@ -366,6 +366,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_first_name"] = "CISA-first-name"
|
||||||
|
additional_details_form["additional_details-cisa_representative_last_name"] = "CISA-last-name"
|
||||||
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||||
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||||
|
|
||||||
|
@ -374,6 +376,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
additional_details_result = additional_details_form.submit()
|
additional_details_result = additional_details_form.submit()
|
||||||
# validate that data from this step are being saved
|
# validate that data from this step are being saved
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.cisa_representative_first_name, "CISA-first-name")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, "CISA-last-name")
|
||||||
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||||
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
|
@ -719,6 +723,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_first_name"] = "cisa-first-name"
|
||||||
|
additional_details_form["additional_details-cisa_representative_last_name"] = "cisa-last-name"
|
||||||
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||||
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||||
|
|
||||||
|
@ -727,6 +733,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
additional_details_result = additional_details_form.submit()
|
additional_details_result = additional_details_form.submit()
|
||||||
# validate that data from this step are being saved
|
# validate that data from this step are being saved
|
||||||
domain_request = DomainRequest.objects.get() # there's only one
|
domain_request = DomainRequest.objects.get() # there's only one
|
||||||
|
self.assertEqual(domain_request.cisa_representative_first_name, "cisa-first-name")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, "cisa-last-name")
|
||||||
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||||
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||||
# the post request should return a redirect to the next form in
|
# the post request should return a redirect to the next form in
|
||||||
|
@ -1125,11 +1133,10 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
|
def test_yes_no_form_inits_yes_for_cisa_representative_and_anything_else(self):
|
||||||
"""On the Additional Details page, the yes/no form gets initialized with YES selected
|
"""On the Additional Details page, the yes/no form gets initialized with YES selected
|
||||||
for both yes/no radios if the domain request has a value for cisa_representative and
|
for both yes/no radios if the domain request has a values for cisa_representative_first_name and
|
||||||
anything_else"""
|
anything_else"""
|
||||||
|
|
||||||
domain_request = completed_domain_request(user=self.user, has_anything_else=True)
|
domain_request = completed_domain_request(user=self.user, has_anything_else=True, has_cisa_representative=True)
|
||||||
domain_request.cisa_representative_email = "test@igorville.gov"
|
|
||||||
domain_request.anything_else = "1234"
|
domain_request.anything_else = "1234"
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
|
@ -1181,12 +1188,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
"""On the Additional details page, the form preselects "no" when has_cisa_representative
|
"""On the Additional details page, the form preselects "no" when has_cisa_representative
|
||||||
and anything_else is no"""
|
and anything_else is no"""
|
||||||
|
|
||||||
domain_request = completed_domain_request(user=self.user, has_anything_else=False)
|
domain_request = completed_domain_request(
|
||||||
|
user=self.user, has_anything_else=False, has_cisa_representative=False
|
||||||
|
)
|
||||||
|
|
||||||
# Unlike the other contacts form, the no button is tracked with these boolean fields.
|
# Unlike the other contacts form, the no button is tracked with these boolean fields.
|
||||||
# This means that we should expect this to correlate with the no button.
|
# This means that we should expect this to correlate with the no button.
|
||||||
domain_request.has_anything_else_text = False
|
domain_request.has_anything_else_text = False
|
||||||
domain_request.has_cisa_representative = False
|
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
# prime the form by visiting /edit
|
# prime the form by visiting /edit
|
||||||
|
@ -1205,7 +1213,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
# Check the cisa representative yes/no field
|
# Check the cisa representative yes/no field
|
||||||
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
yes_no_cisa = additional_details_form["additional_details-has_cisa_representative"].value
|
||||||
self.assertEquals(yes_no_cisa, "False")
|
self.assertEquals(yes_no_cisa, None)
|
||||||
|
|
||||||
# Check the anything else yes/no field
|
# Check the anything else yes/no field
|
||||||
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
yes_no_anything_else = additional_details_form["additional_details-has_anything_else_text"].value
|
||||||
|
@ -1215,11 +1223,15 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
"""When a user submits the Additional Details form with no selected for all fields,
|
"""When a user submits the Additional Details form with no selected for all fields,
|
||||||
the domain request's data gets wiped when submitted"""
|
the domain request's data gets wiped when submitted"""
|
||||||
domain_request = completed_domain_request(name="nocisareps.gov", user=self.user)
|
domain_request = completed_domain_request(name="nocisareps.gov", user=self.user)
|
||||||
|
domain_request.cisa_representative_first_name = "cisa-firstname1"
|
||||||
|
domain_request.cisa_representative_last_name = "cisa-lastname1"
|
||||||
domain_request.cisa_representative_email = "fake@faketown.gov"
|
domain_request.cisa_representative_email = "fake@faketown.gov"
|
||||||
domain_request.save()
|
domain_request.save()
|
||||||
|
|
||||||
# Make sure we have the data we need for the test
|
# Make sure we have the data we need for the test
|
||||||
self.assertEqual(domain_request.anything_else, "There is more")
|
self.assertEqual(domain_request.anything_else, "There is more")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_first_name, "cisa-firstname1")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, "cisa-lastname1")
|
||||||
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
|
self.assertEqual(domain_request.cisa_representative_email, "fake@faketown.gov")
|
||||||
|
|
||||||
# prime the form by visiting /edit
|
# prime the form by visiting /edit
|
||||||
|
@ -1253,25 +1265,31 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
# Verify that the anything_else and cisa_representative have been deleted from the DB
|
# Verify that the anything_else and cisa_representative information have been deleted from the DB
|
||||||
domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov")
|
domain_request = DomainRequest.objects.get(requested_domain__name="nocisareps.gov")
|
||||||
|
|
||||||
# Check that our data has been cleared
|
# Check that our data has been cleared
|
||||||
self.assertEqual(domain_request.anything_else, None)
|
self.assertEqual(domain_request.anything_else, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_first_name, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, None)
|
||||||
self.assertEqual(domain_request.cisa_representative_email, None)
|
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||||
|
|
||||||
# Double check the yes/no fields
|
# Double check the yes/no fields
|
||||||
self.assertEqual(domain_request.has_anything_else_text, False)
|
self.assertEqual(domain_request.has_anything_else_text, False)
|
||||||
self.assertEqual(domain_request.has_cisa_representative, False)
|
self.assertEqual(domain_request.cisa_representative_first_name, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, None)
|
||||||
|
self.assertEqual(domain_request.cisa_representative_email, None)
|
||||||
|
|
||||||
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
|
def test_submitting_additional_details_populates_cisa_representative_and_anything_else(self):
|
||||||
"""When a user submits the Additional Details form,
|
"""When a user submits the Additional Details form,
|
||||||
the domain request's data gets submitted"""
|
the domain request's data gets submitted"""
|
||||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
domain_request = completed_domain_request(
|
||||||
|
name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
|
||||||
|
)
|
||||||
|
|
||||||
# Make sure we have the data we need for the test
|
# Make sure we have the data we need for the test
|
||||||
self.assertEqual(domain_request.anything_else, None)
|
self.assertEqual(domain_request.anything_else, None)
|
||||||
self.assertEqual(domain_request.cisa_representative_email, None)
|
self.assertEqual(domain_request.cisa_representative_first_name, None)
|
||||||
|
|
||||||
# These fields should not be selected at all, since we haven't initialized the form yet
|
# These fields should not be selected at all, since we haven't initialized the form yet
|
||||||
self.assertEqual(domain_request.has_anything_else_text, None)
|
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||||
|
@ -1294,6 +1312,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
# Set fields to true, and set data on those fields
|
# Set fields to true, and set data on those fields
|
||||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||||
|
additional_details_form["additional_details-cisa_representative_first_name"] = "cisa-firstname"
|
||||||
|
additional_details_form["additional_details-cisa_representative_last_name"] = "cisa-lastname"
|
||||||
additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
|
additional_details_form["additional_details-cisa_representative_email"] = "test@faketest.gov"
|
||||||
additional_details_form["additional_details-anything_else"] = "redandblue"
|
additional_details_form["additional_details-anything_else"] = "redandblue"
|
||||||
|
|
||||||
|
@ -1302,10 +1322,12 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
# Verify that the anything_else and cisa_representative exist in the db
|
# Verify that the anything_else and cisa_representative information exist in the db
|
||||||
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
|
domain_request = DomainRequest.objects.get(requested_domain__name="cisareps.gov")
|
||||||
|
|
||||||
self.assertEqual(domain_request.anything_else, "redandblue")
|
self.assertEqual(domain_request.anything_else, "redandblue")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_first_name, "cisa-firstname")
|
||||||
|
self.assertEqual(domain_request.cisa_representative_last_name, "cisa-lastname")
|
||||||
self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov")
|
self.assertEqual(domain_request.cisa_representative_email, "test@faketest.gov")
|
||||||
|
|
||||||
self.assertEqual(domain_request.has_cisa_representative, True)
|
self.assertEqual(domain_request.has_cisa_representative, True)
|
||||||
|
@ -1313,7 +1335,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
|
def test_if_cisa_representative_yes_no_form_is_yes_then_field_is_required(self):
|
||||||
"""Applicants with a cisa representative must provide a value"""
|
"""Applicants with a cisa representative must provide a value"""
|
||||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
domain_request = completed_domain_request(
|
||||||
|
name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
|
||||||
|
)
|
||||||
|
|
||||||
# prime the form by visiting /edit
|
# prime the form by visiting /edit
|
||||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||||
|
@ -1338,7 +1362,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
self.assertContains(response, "Enter the email address of your CISA regional representative.")
|
self.assertContains(response, "Enter the first name / given name of the CISA regional representative.")
|
||||||
|
self.assertContains(response, "Enter the last name / family name of the CISA regional representative.")
|
||||||
|
|
||||||
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
|
def test_if_anything_else_yes_no_form_is_yes_then_field_is_required(self):
|
||||||
"""Applicants with a anything else must provide a value"""
|
"""Applicants with a anything else must provide a value"""
|
||||||
|
@ -1373,7 +1398,9 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
def test_additional_details_form_fields_required(self):
|
def test_additional_details_form_fields_required(self):
|
||||||
"""When a user submits the Additional Details form without checking the
|
"""When a user submits the Additional Details form without checking the
|
||||||
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
|
has_cisa_representative and has_anything_else_text fields, the form should deny this action"""
|
||||||
domain_request = completed_domain_request(name="cisareps.gov", user=self.user, has_anything_else=False)
|
domain_request = completed_domain_request(
|
||||||
|
name="cisareps.gov", user=self.user, has_anything_else=False, has_cisa_representative=False
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEqual(domain_request.has_anything_else_text, None)
|
self.assertEqual(domain_request.has_anything_else_text, None)
|
||||||
self.assertEqual(domain_request.has_cisa_representative, None)
|
self.assertEqual(domain_request.has_cisa_representative, None)
|
||||||
|
|
|
@ -5,3 +5,8 @@ class BranchChoices(models.TextChoices):
|
||||||
EXECUTIVE = "executive", "Executive"
|
EXECUTIVE = "executive", "Executive"
|
||||||
JUDICIAL = "judicial", "Judicial"
|
JUDICIAL = "judicial", "Judicial"
|
||||||
LEGISLATIVE = "legislative", "Legislative"
|
LEGISLATIVE = "legislative", "Legislative"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_branch_label(cls, branch_name: str):
|
||||||
|
"""Returns the associated label for a given org name"""
|
||||||
|
return cls(branch_name).label if branch_name else None
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
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 import (
|
||||||
from registrar.models.domain_invitation import DomainInvitation
|
Domain,
|
||||||
from registrar.models.domain_request import DomainRequest
|
DomainInvitation,
|
||||||
from registrar.models.domain_information import DomainInformation
|
DomainRequest,
|
||||||
|
DomainInformation,
|
||||||
|
PublicContact,
|
||||||
|
UserDomainRole,
|
||||||
|
)
|
||||||
|
from django.db.models import QuerySet, Value, CharField, Count, Q, F
|
||||||
|
from django.db.models import ManyToManyField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db.models import F, Value, CharField
|
|
||||||
from django.db.models.functions import Concat, Coalesce
|
from django.db.models.functions import Concat, Coalesce
|
||||||
|
from django.contrib.postgres.aggregates import StringAgg
|
||||||
from registrar.models.public_contact import PublicContact
|
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.templatetags.custom_filters import get_region
|
||||||
from registrar.utility.enums import DefaultEmail
|
from registrar.utility.enums import DefaultEmail
|
||||||
|
from registrar.utility.constants import BranchChoices
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -299,88 +306,11 @@ def write_csv_for_domains(
|
||||||
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_row_for_requests(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_csv_for_requests(
|
|
||||||
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)
|
|
||||||
total_body_rows = []
|
|
||||||
|
|
||||||
for page_num in paginator.page_range:
|
|
||||||
page = paginator.page(page_num)
|
|
||||||
rows = []
|
|
||||||
for request in page.object_list:
|
|
||||||
try:
|
|
||||||
row = parse_row_for_requests(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
|
|
||||||
total_body_rows.extend(rows)
|
|
||||||
|
|
||||||
if should_write_header:
|
|
||||||
write_header(writer, columns)
|
|
||||||
writer.writerows(total_body_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.
|
||||||
This maps to the "All domain metadata" button.
|
This maps to the "All domain metadata" button.
|
||||||
|
Exports domains of all statuses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
writer = csv.writer(csv_file)
|
writer = csv.writer(csv_file)
|
||||||
|
@ -408,15 +338,8 @@ def export_data_type_to_csv(csv_file):
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
]
|
]
|
||||||
filter_condition = {
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.READY,
|
|
||||||
Domain.State.DNS_NEEDED,
|
|
||||||
Domain.State.ON_HOLD,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
write_csv_for_domains(
|
write_csv_for_domains(
|
||||||
writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True
|
writer, columns, sort_fields, filter_condition={}, should_get_domain_managers=True, should_write_header=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -781,7 +704,44 @@ def export_data_unmanaged_domains_to_csv(csv_file, start_date, end_date):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
|
class DomainRequestExport:
|
||||||
|
"""
|
||||||
|
A collection of functions which return csv files regarding the DomainRequest model.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get all columns on the full metadata report
|
||||||
|
all_columns = [
|
||||||
|
"Domain request",
|
||||||
|
"Submitted at",
|
||||||
|
"Status",
|
||||||
|
"Domain type",
|
||||||
|
"Federal type",
|
||||||
|
"Federal agency",
|
||||||
|
"Organization name",
|
||||||
|
"Election office",
|
||||||
|
"City",
|
||||||
|
"State/territory",
|
||||||
|
"Region",
|
||||||
|
"Creator first name",
|
||||||
|
"Creator last name",
|
||||||
|
"Creator email",
|
||||||
|
"Creator approved domains count",
|
||||||
|
"Creator active requests count",
|
||||||
|
"Alternative domains",
|
||||||
|
"AO first name",
|
||||||
|
"AO last name",
|
||||||
|
"AO email",
|
||||||
|
"AO title/role",
|
||||||
|
"Request purpose",
|
||||||
|
"Request additional details",
|
||||||
|
"Other contacts",
|
||||||
|
"CISA regional representative",
|
||||||
|
"Current websites",
|
||||||
|
"Investigator",
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_data_requests_growth_to_csv(cls, 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.
|
||||||
|
@ -794,10 +754,12 @@ def export_data_requests_growth_to_csv(csv_file, start_date, 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 = [
|
||||||
"Requested domain",
|
"Domain request",
|
||||||
"Organization type",
|
"Domain type",
|
||||||
"Submission date",
|
"Federal type",
|
||||||
|
"Submitted at",
|
||||||
]
|
]
|
||||||
|
|
||||||
sort_fields = [
|
sort_fields = [
|
||||||
"requested_domain__name",
|
"requested_domain__name",
|
||||||
]
|
]
|
||||||
|
@ -807,4 +769,273 @@ def export_data_requests_growth_to_csv(csv_file, start_date, end_date):
|
||||||
"submission_date__gte": start_date_formatted,
|
"submission_date__gte": start_date_formatted,
|
||||||
}
|
}
|
||||||
|
|
||||||
write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True)
|
# We don't want to annotate anything, but we do want to access the requested domain name
|
||||||
|
annotations = {}
|
||||||
|
additional_values = ["requested_domain__name"]
|
||||||
|
|
||||||
|
all_requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
||||||
|
|
||||||
|
annotated_requests = cls.annotate_and_retrieve_fields(all_requests, annotations, additional_values)
|
||||||
|
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||||
|
|
||||||
|
cls.write_csv_for_requests(writer, columns, requests_dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_full_domain_request_report(cls, csv_file):
|
||||||
|
"""
|
||||||
|
Generates a detailed domain request report to a CSV file.
|
||||||
|
|
||||||
|
Retrieves and annotates DomainRequest objects, excluding 'STARTED' status,
|
||||||
|
with related data optimizations via select/prefetch and annotation.
|
||||||
|
|
||||||
|
Annotated with counts and aggregates of related entities.
|
||||||
|
Converts to dict and writes to CSV using predefined columns.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
csv_file (file-like object): Target CSV file.
|
||||||
|
"""
|
||||||
|
writer = csv.writer(csv_file)
|
||||||
|
|
||||||
|
requests = (
|
||||||
|
DomainRequest.objects.select_related(
|
||||||
|
"creator", "authorizing_official", "federal_agency", "investigator", "requested_domain"
|
||||||
|
)
|
||||||
|
.prefetch_related("current_websites", "other_contacts", "alternative_domains")
|
||||||
|
.exclude(status__in=[DomainRequest.DomainRequestStatus.STARTED])
|
||||||
|
.order_by(
|
||||||
|
"status",
|
||||||
|
"requested_domain__name",
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Annotations are custom columns returned to the queryset (AKA: computed in the DB).
|
||||||
|
annotations = cls._full_domain_request_annotations()
|
||||||
|
|
||||||
|
# The .values returned from annotate_and_retrieve_fields can't go two levels deep
|
||||||
|
# (just returns the field id of say, "creator") - so we have to include this.
|
||||||
|
additional_values = [
|
||||||
|
"requested_domain__name",
|
||||||
|
"federal_agency__agency",
|
||||||
|
"authorizing_official__first_name",
|
||||||
|
"authorizing_official__last_name",
|
||||||
|
"authorizing_official__email",
|
||||||
|
"authorizing_official__title",
|
||||||
|
"creator__first_name",
|
||||||
|
"creator__last_name",
|
||||||
|
"creator__email",
|
||||||
|
"investigator__email",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Convert the domain request queryset to a dictionary (including annotated fields)
|
||||||
|
annotated_requests = cls.annotate_and_retrieve_fields(requests, annotations, additional_values)
|
||||||
|
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
||||||
|
|
||||||
|
# Write the csv file
|
||||||
|
cls.write_csv_for_requests(writer, cls.all_columns, requests_dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _full_domain_request_annotations(cls, delimiter=" | "):
|
||||||
|
"""Returns the annotations for the full domain request report"""
|
||||||
|
return {
|
||||||
|
"creator_approved_domains_count": DomainRequestExport.get_creator_approved_domains_count_query(),
|
||||||
|
"creator_active_requests_count": DomainRequestExport.get_creator_active_requests_count_query(),
|
||||||
|
"all_current_websites": StringAgg("current_websites__website", delimiter=delimiter, distinct=True),
|
||||||
|
"all_alternative_domains": StringAgg("alternative_domains__website", delimiter=delimiter, distinct=True),
|
||||||
|
# Coerce the other contacts object to "{first_name} {last_name} {email}"
|
||||||
|
"all_other_contacts": StringAgg(
|
||||||
|
Concat(
|
||||||
|
"other_contacts__first_name",
|
||||||
|
Value(" "),
|
||||||
|
"other_contacts__last_name",
|
||||||
|
Value(" "),
|
||||||
|
"other_contacts__email",
|
||||||
|
),
|
||||||
|
delimiter=delimiter,
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_csv_for_requests(
|
||||||
|
writer,
|
||||||
|
columns,
|
||||||
|
requests_dict,
|
||||||
|
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."""
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for request in requests_dict.values():
|
||||||
|
try:
|
||||||
|
row = DomainRequestExport.parse_row_for_requests(columns, request)
|
||||||
|
rows.append(row)
|
||||||
|
except ValueError as err:
|
||||||
|
logger.error(f"csv_export -> Error when parsing row: {err}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if should_write_header:
|
||||||
|
write_header(writer, columns)
|
||||||
|
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_row_for_requests(columns, request):
|
||||||
|
"""
|
||||||
|
Given a set of columns and a request dictionary, generate a new row from cleaned column data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Handle the federal_type field. Defaults to the wrong format.
|
||||||
|
federal_type = request.get("federal_type")
|
||||||
|
human_readable_federal_type = BranchChoices.get_branch_label(federal_type) if federal_type else None
|
||||||
|
|
||||||
|
# Handle the org_type field
|
||||||
|
org_type = request.get("generic_org_type") or request.get("organization_type")
|
||||||
|
human_readable_org_type = DomainRequest.OrganizationChoices.get_org_label(org_type) if org_type else None
|
||||||
|
|
||||||
|
# Handle the status field. Defaults to the wrong format.
|
||||||
|
status = request.get("status")
|
||||||
|
status_display = DomainRequest.DomainRequestStatus.get_status_label(status) if status else None
|
||||||
|
|
||||||
|
# Handle the region field.
|
||||||
|
state_territory = request.get("state_territory")
|
||||||
|
region = get_region(state_territory) if state_territory else None
|
||||||
|
|
||||||
|
# Handle the requested_domain field (add a default if None)
|
||||||
|
requested_domain = request.get("requested_domain__name")
|
||||||
|
requested_domain_name = requested_domain if requested_domain else "No requested domain"
|
||||||
|
|
||||||
|
# Handle the election field. N/A if None, "Yes"/"No" if boolean
|
||||||
|
human_readable_election_board = "N/A"
|
||||||
|
is_election_board = request.get("is_election_board")
|
||||||
|
if is_election_board is not None:
|
||||||
|
human_readable_election_board = "Yes" if is_election_board else "No"
|
||||||
|
|
||||||
|
# Handle the additional details field. Pipe seperated.
|
||||||
|
cisa_rep_first = request.get("cisa_representative_first_name")
|
||||||
|
cisa_rep_last = request.get("cisa_representative_last_name")
|
||||||
|
name = [n for n in [cisa_rep_first, cisa_rep_last] if n]
|
||||||
|
|
||||||
|
cisa_rep = " ".join(name) if name else None
|
||||||
|
details = [cisa_rep, request.get("anything_else")]
|
||||||
|
additional_details = " | ".join([field for field in details if field])
|
||||||
|
|
||||||
|
# create a dictionary of fields which can be included in output.
|
||||||
|
# "extra_fields" are precomputed fields (generated in the DB or parsed).
|
||||||
|
FIELDS = {
|
||||||
|
# Parsed fields - defined above.
|
||||||
|
"Domain request": requested_domain_name,
|
||||||
|
"Region": region,
|
||||||
|
"Status": status_display,
|
||||||
|
"Election office": human_readable_election_board,
|
||||||
|
"Federal type": human_readable_federal_type,
|
||||||
|
"Domain type": human_readable_org_type,
|
||||||
|
"Request additional details": additional_details,
|
||||||
|
# Annotated fields - passed into the request dict.
|
||||||
|
"Creator approved domains count": request.get("creator_approved_domains_count", 0),
|
||||||
|
"Creator active requests count": request.get("creator_active_requests_count", 0),
|
||||||
|
"Alternative domains": request.get("all_alternative_domains"),
|
||||||
|
"Other contacts": request.get("all_other_contacts"),
|
||||||
|
"Current websites": request.get("all_current_websites"),
|
||||||
|
# Untouched FK fields - passed into the request dict.
|
||||||
|
"Federal agency": request.get("federal_agency__agency"),
|
||||||
|
"AO first name": request.get("authorizing_official__first_name"),
|
||||||
|
"AO last name": request.get("authorizing_official__last_name"),
|
||||||
|
"AO email": request.get("authorizing_official__email"),
|
||||||
|
"AO title/role": request.get("authorizing_official__title"),
|
||||||
|
"Creator first name": request.get("creator__first_name"),
|
||||||
|
"Creator last name": request.get("creator__last_name"),
|
||||||
|
"Creator email": request.get("creator__email"),
|
||||||
|
"Investigator": request.get("investigator__email"),
|
||||||
|
# Untouched fields
|
||||||
|
"Organization name": request.get("organization_name"),
|
||||||
|
"City": request.get("city"),
|
||||||
|
"State/territory": request.get("state_territory"),
|
||||||
|
"Request purpose": request.get("purpose"),
|
||||||
|
"CISA regional representative": request.get("cisa_representative_email"),
|
||||||
|
"Submitted at": request.get("submission_date"),
|
||||||
|
}
|
||||||
|
|
||||||
|
row = [FIELDS.get(column, "") for column in columns]
|
||||||
|
return row
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def annotate_and_retrieve_fields(
|
||||||
|
cls, requests, annotations, additional_values=None, include_many_to_many=False
|
||||||
|
) -> QuerySet:
|
||||||
|
"""
|
||||||
|
Applies annotations to a queryset and retrieves specified fields,
|
||||||
|
including class-defined and annotation-defined.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
requests (QuerySet): Initial queryset.
|
||||||
|
annotations (dict, optional): Fields to compute {field_name: expression}.
|
||||||
|
additional_values (list, optional): Extra fields to retrieve; defaults to annotation keys if None.
|
||||||
|
include_many_to_many (bool, optional): Determines if we should include many to many fields or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QuerySet: Contains dictionaries with the specified fields for each record.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if additional_values is None:
|
||||||
|
additional_values = []
|
||||||
|
|
||||||
|
# We can infer that if we're passing in annotations,
|
||||||
|
# we want to grab the result of said annotation.
|
||||||
|
if annotations:
|
||||||
|
additional_values.extend(annotations.keys())
|
||||||
|
|
||||||
|
# Get prexisting fields on DomainRequest
|
||||||
|
domain_request_fields = set()
|
||||||
|
for field in DomainRequest._meta.get_fields():
|
||||||
|
# Exclude many to many fields unless we specify
|
||||||
|
many_to_many = isinstance(field, ManyToManyField) and include_many_to_many
|
||||||
|
if many_to_many or not isinstance(field, ManyToManyField):
|
||||||
|
domain_request_fields.add(field.name)
|
||||||
|
|
||||||
|
queryset = requests.annotate(**annotations).values(*domain_request_fields, *additional_values)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
# ============================================================= #
|
||||||
|
# Helper functions for django ORM queries. #
|
||||||
|
# We are using these rather than pure python for speed reasons. #
|
||||||
|
# ============================================================= #
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_creator_approved_domains_count_query():
|
||||||
|
"""
|
||||||
|
Generates a Count query for distinct approved domain requests per creator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Count: Aggregates distinct 'APPROVED' domain requests by creator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Count(
|
||||||
|
"creator__domain_requests_created__id",
|
||||||
|
filter=Q(creator__domain_requests_created__status=DomainRequest.DomainRequestStatus.APPROVED),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_creator_active_requests_count_query():
|
||||||
|
"""
|
||||||
|
Generates a Count query for distinct approved domain requests per creator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Count: Aggregates distinct 'SUBMITTED', 'IN_REVIEW', and 'ACTION_NEEDED' domain requests by creator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = Count(
|
||||||
|
"creator__domain_requests_created__id",
|
||||||
|
filter=Q(
|
||||||
|
creator__domain_requests_created__status__in=[
|
||||||
|
DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|
|
@ -164,6 +164,17 @@ class ExportDataFederal(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDomainRequestDataFull(View):
|
||||||
|
"""Generates a downloaded report containing all Domain Requests (except started)"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Returns a content disposition response for current-full-domain-request.csv"""
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="current-full-domain-request.csv"'
|
||||||
|
csv_export.DomainRequestExport.export_full_domain_request_report(response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataDomainsGrowth(View):
|
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
|
||||||
|
@ -191,7 +202,7 @@ class ExportDataRequestsGrowth(View):
|
||||||
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.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
|
# 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_requests_growth_to_csv(response, start_date, end_date)
|
csv_export.DomainRequestExport.export_data_requests_growth_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
|
@ -369,7 +369,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
or self.domain_request.no_other_contacts_rationale is not None
|
or self.domain_request.no_other_contacts_rationale is not None
|
||||||
),
|
),
|
||||||
"additional_details": (
|
"additional_details": (
|
||||||
(self.domain_request.anything_else is not None and self.domain_request.cisa_representative_email)
|
(self.domain_request.anything_else is not None and self.domain_request.has_cisa_representative)
|
||||||
or self.domain_request.is_policy_acknowledged is not None
|
or self.domain_request.is_policy_acknowledged is not None
|
||||||
),
|
),
|
||||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||||
|
@ -380,7 +380,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Define context for access on all wizard pages."""
|
"""Define context for access on all wizard pages."""
|
||||||
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
||||||
logger.debug("PROFILE FLAG is %s" % has_profile_flag)
|
|
||||||
|
|
||||||
context_stuff = {}
|
context_stuff = {}
|
||||||
if DomainRequest._form_complete(self.domain_request):
|
if DomainRequest._form_complete(self.domain_request):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue