Merge remote-tracking branch 'origin/main' into nl/2248-add-org-and-portfolio-table

This commit is contained in:
CocoByte 2024-06-13 15:53:54 -06:00
commit d7de91b7f9
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
21 changed files with 876 additions and 208 deletions

View file

@ -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 = [

View file

@ -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;
}
}

View file

@ -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(),

View file

@ -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 representatives email (optional)",
max_length=None, max_length=None,
label="Your representatives email", required=False,
error_messages={
"invalid": ("Enter your representatives 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."),
},
) )

View file

@ -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"
),
),
]

View file

@ -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)

View file

@ -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
has_details = False
if self.has_anything_else_text or self.has_cisa_representative:
has_details = True
# Split out for linter
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

View file

@ -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

View file

@ -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>

View file

@ -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 %}

View file

@ -157,18 +157,33 @@
{% 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">
{% if domain_request.anything_else %} {% if domain_request.anything_else %}
{{domain_request.anything_else}} {{domain_request.anything_else}}
{% else %} {% else %}
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 %}

View file

@ -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>

View file

@ -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

View file

@ -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",
] ]

View file

@ -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,

View file

@ -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,10 @@ 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, # Normalize line endings and remove commas,
@ -699,6 +704,72 @@ class ExportDataTest(MockDb, MockEppLib):
self.assertEqual(csv_content, expected_content) 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,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
class HelperFunctions(MockDb): class HelperFunctions(MockDb):
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy.""" """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""
@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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,30 +704,338 @@ 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:
""" """
Growth report: A collection of functions which return csv files regarding the DomainRequest model.
Receive start and end dates from the view, parse them.
Request from write_requests_body SUBMITTED requests that are created between
the start and end dates. Specify sort params.
""" """
start_date_formatted = format_start_date(start_date) # Get all columns on the full metadata report
end_date_formatted = format_end_date(end_date) all_columns = [
writer = csv.writer(csv_file) "Domain request",
# define columns to include in export "Submitted at",
columns = [ "Status",
"Requested domain", "Domain type",
"Organization type", "Federal type",
"Submission date", "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",
] ]
sort_fields = [
"requested_domain__name",
]
filter_condition = {
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
"submission_date__lte": end_date_formatted,
"submission_date__gte": start_date_formatted,
}
write_csv_for_requests(writer, columns, sort_fields, filter_condition, should_write_header=True) @classmethod
def export_data_requests_growth_to_csv(cls, csv_file, start_date, end_date):
"""
Growth report:
Receive start and end dates from the view, parse them.
Request from write_requests_body SUBMITTED requests that are created between
the start and end dates. Specify sort params.
"""
start_date_formatted = format_start_date(start_date)
end_date_formatted = format_end_date(end_date)
writer = csv.writer(csv_file)
# define columns to include in export
columns = [
"Domain request",
"Domain type",
"Federal type",
"Submitted at",
]
sort_fields = [
"requested_domain__name",
]
filter_condition = {
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
"submission_date__lte": end_date_formatted,
"submission_date__gte": start_date_formatted,
}
# 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

View file

@ -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

View file

@ -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):