mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +02:00
Merge branch 'main' into za/2166-domain-request-csv-report
This commit is contained in:
commit
7399982aaa
27 changed files with 793 additions and 130 deletions
|
@ -217,6 +217,7 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
status = cleaned_data.get("status")
|
||||
investigator = cleaned_data.get("investigator")
|
||||
rejection_reason = cleaned_data.get("rejection_reason")
|
||||
action_needed_reason = cleaned_data.get("action_needed_reason")
|
||||
|
||||
# Get the old status
|
||||
initial_status = self.initial.get("status", None)
|
||||
|
@ -240,6 +241,8 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
# If the status is rejected, a rejection reason must exist
|
||||
if status == DomainRequest.DomainRequestStatus.REJECTED:
|
||||
self._check_for_valid_rejection_reason(rejection_reason)
|
||||
elif status == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
|
||||
self._check_for_valid_action_needed_reason(action_needed_reason)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
@ -263,6 +266,18 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
|
||||
return is_valid
|
||||
|
||||
def _check_for_valid_action_needed_reason(self, action_needed_reason) -> bool:
|
||||
"""
|
||||
Checks if the action_needed_reason field is not none.
|
||||
Adds form errors on failure.
|
||||
"""
|
||||
is_valid = action_needed_reason is not None and action_needed_reason != ""
|
||||
if not is_valid:
|
||||
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON)
|
||||
self.add_error("action_needed_reason", error_message)
|
||||
|
||||
return is_valid
|
||||
|
||||
def _check_for_valid_investigator(self, investigator) -> bool:
|
||||
"""
|
||||
Checks if the investigator field is not none, and is staff.
|
||||
|
@ -1166,6 +1181,8 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
|||
# error.
|
||||
readonly_fields = ["status"]
|
||||
|
||||
autocomplete_fields = ["domain"]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
|
||||
# Select domain invitations to change -> Domain invitations
|
||||
|
@ -1466,6 +1483,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"fields": [
|
||||
"status",
|
||||
"rejection_reason",
|
||||
"action_needed_reason",
|
||||
"investigator",
|
||||
"creator",
|
||||
"submitter",
|
||||
|
@ -1482,6 +1500,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"authorizing_official",
|
||||
"other_contacts",
|
||||
"no_other_contacts_rationale",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
},
|
||||
|
@ -1557,6 +1577,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
autocomplete_fields = [
|
||||
|
@ -1668,6 +1690,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||
# because we clean up the rejection reason in the transition in the model.
|
||||
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_REJECTION_REASON)
|
||||
elif obj.status == models.DomainRequest.DomainRequestStatus.ACTION_NEEDED and not obj.action_needed_reason:
|
||||
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_ACTION_NEEDED_REASON)
|
||||
else:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we roll back the
|
||||
|
|
|
@ -300,25 +300,29 @@ function initializeWidgetOnList(list, parentId) {
|
|||
*/
|
||||
(function (){
|
||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
||||
|
||||
if (rejectionReasonFormGroup) {
|
||||
if (rejectionReasonFormGroup && actionNeededReasonFormGroup) {
|
||||
let statusSelect = document.getElementById('id_status')
|
||||
let isRejected = statusSelect.value == "rejected"
|
||||
let isActionNeeded = statusSelect.value == "action needed"
|
||||
|
||||
// Initial handling of rejectionReasonFormGroup display
|
||||
if (statusSelect.value != 'rejected')
|
||||
rejectionReasonFormGroup.style.display = 'none';
|
||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||
|
||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||
statusSelect.addEventListener('change', function() {
|
||||
if (statusSelect.value == 'rejected') {
|
||||
rejectionReasonFormGroup.style.display = 'block';
|
||||
sessionStorage.removeItem('hideRejectionReason');
|
||||
} else {
|
||||
rejectionReasonFormGroup.style.display = 'none';
|
||||
sessionStorage.setItem('hideRejectionReason', 'true');
|
||||
}
|
||||
// Show the rejection reason field if the status is rejected.
|
||||
// Then track if its shown or hidden in our session cache.
|
||||
isRejected = statusSelect.value == "rejected"
|
||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
||||
addOrRemoveSessionBoolean("showRejectionReason", add=isRejected)
|
||||
|
||||
isActionNeeded = statusSelect.value == "action needed"
|
||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
||||
});
|
||||
}
|
||||
|
||||
// Listen to Back/Forward button navigation and handle rejectionReasonFormGroup display based on session storage
|
||||
|
||||
|
@ -328,14 +332,34 @@ function initializeWidgetOnList(list, parentId) {
|
|||
const observer = new PerformanceObserver((list) => {
|
||||
list.getEntries().forEach((entry) => {
|
||||
if (entry.type === "back_forward") {
|
||||
if (sessionStorage.getItem('hideRejectionReason'))
|
||||
document.querySelector('.field-rejection_reason').style.display = 'none';
|
||||
else
|
||||
document.querySelector('.field-rejection_reason').style.display = 'block';
|
||||
let showRejectionReason = sessionStorage.getItem("showRejectionReason") !== null
|
||||
showOrHideObject(rejectionReasonFormGroup, show=showRejectionReason)
|
||||
|
||||
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
||||
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe({ type: "navigation" });
|
||||
}
|
||||
|
||||
// Adds or removes the display-none class to object depending on the value of boolean show
|
||||
function showOrHideObject(object, show){
|
||||
if (show){
|
||||
object.classList.remove("display-none");
|
||||
}else {
|
||||
object.classList.add("display-none");
|
||||
}
|
||||
}
|
||||
|
||||
// Adds or removes a boolean from our session
|
||||
function addOrRemoveSessionBoolean(name, add){
|
||||
if (add) {
|
||||
sessionStorage.setItem(name, "true");
|
||||
}else {
|
||||
sessionStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/** An IIFE for toggling the submit bar on domain request forms
|
||||
|
|
|
@ -648,20 +648,27 @@ class NoOtherContactsForm(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(
|
||||
required=True,
|
||||
label="Your representative’s email (optional)",
|
||||
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=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
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,28 @@
|
|||
# Generated by Django 4.2.10 on 2024-06-12 14:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0099_federalagency_federal_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="action_needed_reason",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("eligibility_unclear", "Unclear organization eligibility"),
|
||||
("questionable_authorizing_official", "Questionable authorizing official"),
|
||||
("already_has_domains", "Already has domains"),
|
||||
("bad_name", "Doesn’t meet naming requirements"),
|
||||
("other", "Other (no auto-email sent)"),
|
||||
],
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -214,13 +214,45 @@ class DomainInformation(TimeStampedModel):
|
|||
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(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="CISA regional representative",
|
||||
verbose_name="CISA regional representative email",
|
||||
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(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -241,6 +273,30 @@ class DomainInformation(TimeStampedModel):
|
|||
except Exception:
|
||||
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):
|
||||
"""
|
||||
Updates the organization_type (without saving) to match
|
||||
|
@ -275,6 +331,7 @@ class DomainInformation(TimeStampedModel):
|
|||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
self.sync_yes_no_form_fields()
|
||||
self.sync_organization_type()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
from typing import Union
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
|
@ -263,6 +262,15 @@ class DomainRequest(TimeStampedModel):
|
|||
NAMING_REQUIREMENTS = "naming_not_met", "Naming requirements not met"
|
||||
OTHER = "other", "Other/Unspecified"
|
||||
|
||||
class ActionNeededReasons(models.TextChoices):
|
||||
"""Defines common action needed reasons for domain requests"""
|
||||
|
||||
ELIGIBILITY_UNCLEAR = ("eligibility_unclear", "Unclear organization eligibility")
|
||||
QUESTIONABLE_AUTHORIZING_OFFICIAL = ("questionable_authorizing_official", "Questionable authorizing official")
|
||||
ALREADY_HAS_DOMAINS = ("already_has_domains", "Already has domains")
|
||||
BAD_NAME = ("bad_name", "Doesn’t meet naming requirements")
|
||||
OTHER = ("other", "Other (no auto-email sent)")
|
||||
|
||||
# #### Internal fields about the domain request #####
|
||||
status = FSMField(
|
||||
choices=DomainRequestStatus.choices, # possible states as an array of constants
|
||||
|
@ -276,6 +284,12 @@ class DomainRequest(TimeStampedModel):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
action_needed_reason = models.TextField(
|
||||
choices=ActionNeededReasons.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
federal_agency = models.ForeignKey(
|
||||
"registrar.FederalAgency",
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -476,10 +490,24 @@ class DomainRequest(TimeStampedModel):
|
|||
cisa_representative_email = models.EmailField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="CISA regional representative",
|
||||
verbose_name="CISA regional representative email",
|
||||
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.
|
||||
|
@ -538,6 +566,16 @@ class DomainRequest(TimeStampedModel):
|
|||
# Actually updates the organization_type field
|
||||
org_type_helper.create_or_update_organization_type()
|
||||
|
||||
def _cache_status_and_action_needed_reason(self):
|
||||
"""Maintains a cache of properties so we can avoid a DB call"""
|
||||
self._cached_action_needed_reason = self.action_needed_reason
|
||||
self._cached_status = self.status
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Store original values for caching purposes. Used to compare them on save.
|
||||
self._cache_status_and_action_needed_reason()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Save override for custom properties"""
|
||||
self.sync_organization_type()
|
||||
|
@ -545,20 +583,38 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Handle the action needed email. We send one when moving to action_needed,
|
||||
# but we don't send one when we are _already_ in the state and change the reason.
|
||||
self.sync_action_needed_reason()
|
||||
|
||||
# Update the cached values after saving
|
||||
self._cache_status_and_action_needed_reason()
|
||||
|
||||
def sync_action_needed_reason(self):
|
||||
"""Checks if we need to send another action needed email"""
|
||||
was_already_action_needed = self._cached_status == self.DomainRequestStatus.ACTION_NEEDED
|
||||
reason_exists = self._cached_action_needed_reason is not None and self.action_needed_reason is not None
|
||||
reason_changed = self._cached_action_needed_reason != self.action_needed_reason
|
||||
if was_already_action_needed and (reason_exists and reason_changed):
|
||||
# We don't send emails out in state "other"
|
||||
if self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
||||
self._send_action_needed_reason_email()
|
||||
|
||||
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_email is not None:
|
||||
self.has_cisa_representative = self.cisa_representative_email != ""
|
||||
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_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
|
||||
if self.anything_else is not None:
|
||||
|
@ -596,7 +652,7 @@ class DomainRequest(TimeStampedModel):
|
|||
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||
|
||||
def _send_status_update_email(
|
||||
self, new_status, email_template, email_template_subject, send_email=True, bcc_address=""
|
||||
self, new_status, email_template, email_template_subject, send_email=True, bcc_address="", wrap_email=False
|
||||
):
|
||||
"""Send a status update email to the submitter.
|
||||
|
||||
|
@ -623,6 +679,7 @@ class DomainRequest(TimeStampedModel):
|
|||
self.submitter.email,
|
||||
context={"domain_request": self},
|
||||
bcc_address=bcc_address,
|
||||
wrap_email=wrap_email,
|
||||
)
|
||||
logger.info(f"The {new_status} email sent to: {self.submitter.email}")
|
||||
except EmailSendingError:
|
||||
|
@ -706,9 +763,10 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("in_review")
|
||||
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
elif self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
elif self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
||||
self.action_needed_reason = None
|
||||
|
||||
literal = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
# Check if the tuple exists, then grab its value
|
||||
|
@ -726,7 +784,7 @@ class DomainRequest(TimeStampedModel):
|
|||
target=DomainRequestStatus.ACTION_NEEDED,
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def action_needed(self):
|
||||
def action_needed(self, send_email=True):
|
||||
"""Send back an domain request that is under investigation or rejected.
|
||||
|
||||
This action is logged.
|
||||
|
@ -738,8 +796,7 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
if self.status == self.DomainRequestStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
elif self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
|
||||
literal = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||
|
@ -747,6 +804,46 @@ class DomainRequest(TimeStampedModel):
|
|||
action_needed = literal if literal is not None else "Action Needed"
|
||||
logger.info(f"A status change occurred. {self} was changed to '{action_needed}'")
|
||||
|
||||
# Send out an email if an action needed reason exists
|
||||
if self.action_needed_reason and self.action_needed_reason != self.ActionNeededReasons.OTHER:
|
||||
self._send_action_needed_reason_email(send_email)
|
||||
|
||||
def _send_action_needed_reason_email(self, send_email=True):
|
||||
"""Sends out an automatic email for each valid action needed reason provided"""
|
||||
|
||||
# Store the filenames of the template and template subject
|
||||
email_template_name: str = ""
|
||||
email_template_subject_name: str = ""
|
||||
|
||||
# Check for the "type" of action needed reason.
|
||||
can_send_email = True
|
||||
match self.action_needed_reason:
|
||||
# Add to this match if you need to pass in a custom filename for these templates.
|
||||
case self.ActionNeededReasons.OTHER, _:
|
||||
# Unknown and other are default cases - do nothing
|
||||
can_send_email = False
|
||||
|
||||
# Assumes that the template name matches the action needed reason if nothing is specified.
|
||||
# This is so you can override if you need, or have this taken care of for you.
|
||||
if not email_template_name and not email_template_subject_name:
|
||||
email_template_name = f"{self.action_needed_reason}.txt"
|
||||
email_template_subject_name = f"{self.action_needed_reason}_subject.txt"
|
||||
|
||||
bcc_address = ""
|
||||
if settings.IS_PRODUCTION:
|
||||
bcc_address = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# If we can, try to send out an email as long as send_email=True
|
||||
if can_send_email:
|
||||
self._send_status_update_email(
|
||||
new_status="action needed",
|
||||
email_template=f"emails/action_needed_reasons/{email_template_name}",
|
||||
email_template_subject=f"emails/action_needed_reasons/{email_template_subject_name}",
|
||||
send_email=send_email,
|
||||
bcc_address=bcc_address,
|
||||
wrap_email=True,
|
||||
)
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
|
@ -795,6 +892,8 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
elif self.status == self.DomainRequestStatus.ACTION_NEEDED:
|
||||
self.action_needed_reason = None
|
||||
|
||||
# == Send out an email == #
|
||||
self._send_status_update_email(
|
||||
|
@ -913,11 +1012,12 @@ class DomainRequest(TimeStampedModel):
|
|||
def has_additional_details(self) -> bool:
|
||||
"""Combines the has_anything_else_text and has_cisa_representative fields,
|
||||
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
|
||||
|
||||
if self.has_anything_else_text is None or self.has_cisa_representative is None:
|
||||
has_details = False
|
||||
return has_details
|
||||
|
||||
def is_federal(self) -> Union[bool, None]:
|
||||
|
@ -1026,14 +1126,19 @@ class DomainRequest(TimeStampedModel):
|
|||
return True
|
||||
return False
|
||||
|
||||
def _cisa_rep_and_email_check(self):
|
||||
# Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep
|
||||
return (
|
||||
def _cisa_rep_check(self):
|
||||
# Either does not have a CISA rep, OR has a CISA rep + both first name
|
||||
# and last name are NOT empty and are NOT an empty string
|
||||
to_return = (
|
||||
self.has_cisa_representative is True
|
||||
and self.cisa_representative_email is not None
|
||||
and self.cisa_representative_email != ""
|
||||
and self.cisa_representative_first_name is not None
|
||||
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
|
||||
|
||||
return to_return
|
||||
|
||||
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
|
||||
return (
|
||||
|
@ -1041,7 +1146,7 @@ class DomainRequest(TimeStampedModel):
|
|||
) or self.has_anything_else_text is False
|
||||
|
||||
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):
|
||||
return self.is_policy_acknowledged is not None
|
||||
|
|
|
@ -69,8 +69,8 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
|
||||
{% block after_help_text %}
|
||||
{% if field.field.name == "status" and original_object.history.count > 0 %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Submitter contact details"></label>
|
||||
<div class="flex-container" id="dja-status-changelog">
|
||||
<label aria-label="Status changelog"></label>
|
||||
<div>
|
||||
<div class="usa-table-container--scrollable collapse--dgsimple" tabindex="0">
|
||||
<table class="usa-table usa-table--borderless">
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
{# commented out so it does not appear at this point on this page #}
|
||||
{% endblock %}
|
||||
|
||||
<!-- TODO-NL: (refactor) Breakup into two separate components-->
|
||||
{% block form_fields %}
|
||||
<fieldset class="usa-fieldset margin-top-2">
|
||||
<legend>
|
||||
|
@ -22,13 +21,13 @@
|
|||
{% input_with_errors forms.0.has_cisa_representative %}
|
||||
{% endwith %}
|
||||
{# 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>
|
||||
|
||||
<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 %}
|
||||
{# 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>
|
||||
|
||||
|
||||
|
@ -42,7 +41,6 @@
|
|||
{% input_with_errors forms.2.has_anything_else_text %}
|
||||
{% endwith %}
|
||||
{# 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>
|
||||
|
||||
<div id="anything-else">
|
||||
|
@ -50,6 +48,5 @@
|
|||
{% input_with_errors forms.3.anything_else %}
|
||||
{% endwith %}
|
||||
{# 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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -157,9 +157,20 @@
|
|||
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% 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 %}
|
||||
{% 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' %}
|
||||
{% endwith %}
|
||||
{% with title=form_titles|get_item:step %}
|
||||
{% if domain_request.has_additional_details %}
|
||||
{% 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>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
|
@ -169,6 +180,10 @@
|
|||
No
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
||||
|
||||
|
|
|
@ -118,7 +118,15 @@
|
|||
|
||||
{# We always show this field even if None #}
|
||||
{% 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>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
|
@ -128,7 +136,6 @@
|
|||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Action needed
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
ORGANIZATION ALREADY HAS A .GOV DOMAIN
|
||||
We've reviewed your domain request, but your organization already has at least one other .gov domain. We need more information about your rationale for registering another .gov domain.
|
||||
In general, there are two reasons we will approve an additional domain:
|
||||
- You determine a current .gov domain name will be replaced
|
||||
- We determine an additional domain name is appropriate
|
||||
|
||||
|
||||
WE LIMIT ADDITIONAL DOMAIN NAMES
|
||||
Our practice is to only approve one domain per online service per government organization, evaluating additional requests on a case-by-case basis.
|
||||
There are two core reasons we limit additional domains:
|
||||
- We want to minimize your operational and security load, which increases with each additional domain.
|
||||
- Fewer domains allow us to take protective, namespace-wide security actions faster and without undue dependencies.
|
||||
|
||||
If you’re attempting to claim an additional domain to prevent others from obtaining it, that’s not necessary. .Gov domains are only available to U.S.-based government organizations, and we don’t operate on a first come, first served basis. We'll only assign a domain to the organization whose real name or services actually correspond to the domain name.
|
||||
|
||||
|
||||
CONSIDER USING A SUBDOMAIN
|
||||
Using a subdomain of an existing domain (e.g., service.domain.gov) is a common approach to logically divide your namespace while still maintaining an association with your existing domain name. Subdomains can also be delegated to allow an affiliated entity to manage their own DNS settings.
|
||||
|
||||
|
||||
ACTION NEEDED
|
||||
FOR A REPLACEMENT DOMAIN: If you’re requesting a new domain that will replace your current domain name, we can allow for a transition period where both are registered to your organization. Afterwards, we will reclaim and retire the legacy name.
|
||||
|
||||
Reply to this email. Tell us how many months your organization needs to maintain your current .gov domain and conduct a transition to a new one. Detail why that period of time is needed.
|
||||
|
||||
FOR AN ADDITIONAL DOMAIN: If you’re requesting an additional domain and not replacing your existing one, we’ll need more information to support that request.
|
||||
|
||||
Reply to this email. Detail why you believe another domain is necessary for your organization, and why a subdomain won’t meet your needs.
|
||||
|
||||
|
||||
If you have questions or comments, include those in your reply.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <http://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
|
@ -0,0 +1,34 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Action needed
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS
|
||||
We've reviewed your domain request and, unfortunately, it does not meet our naming requirements.
|
||||
|
||||
Domains should uniquely identify a government organization and be clear to the general public. Read more about naming requirements for your type of organization <https://get.gov/domains/choosing/>.
|
||||
|
||||
|
||||
ACTION NEEDED
|
||||
First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <https://manage.get.gov/> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
|
||||
If you have questions or want to discuss potential domain names, reply to this email.
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <http://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
|
@ -0,0 +1,35 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Action needed
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS
|
||||
We've reviewed your domain request, but we need more information about the organization you represent:
|
||||
- {{ domain_request.organization_name }}
|
||||
|
||||
.Gov domains are only available to official US-based government organizations, not simply those that provide a public benefit. We lack clear documentation that demonstrates your organization is eligible for a .gov domain.
|
||||
|
||||
|
||||
ACTION NEEDED
|
||||
Reply to this email with links to (or copies of) your authorizing legislation, your founding charter or bylaws, recent election results, or other similar documentation. Without this, we can’t continue our review and your request will likely be rejected.
|
||||
|
||||
If you have questions or comments, include those in your reply.
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <http://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
|
@ -0,0 +1,36 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi, {{ domain_request.submitter.first_name }}.
|
||||
|
||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ domain_request.submission_date|date }}
|
||||
STATUS: Action needed
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS
|
||||
We've reviewed your domain request, but we need more information about the authorizing official listed on the request:
|
||||
- {{ domain_request.authorizing_official.get_formatted_name }}
|
||||
- {{ domain_request.authorizing_official.title }}
|
||||
|
||||
We expect an authorizing official to be someone in a role of significant, executive responsibility within the organization. Our guidelines are open-ended to accommodate the wide variety of government organizations that are eligible for .gov domains, but the person you listed does not meet our expectations for your type of organization. Read more about our guidelines for authorizing officials. <https://get.gov/domains/eligibility/>
|
||||
|
||||
|
||||
ACTION NEEDED
|
||||
Reply to this email with a justification for naming {{ domain_request.authorizing_official.get_formatted_name }} as the authorizing official. If you have questions or comments, include those in your reply.
|
||||
|
||||
Alternatively, you can log in to the registrar and enter a different authorizing official for this domain request. <https://manage.get.gov/> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <http://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Update on your .gov request: {{ domain_request.requested_domain.name }}
|
|
@ -842,12 +842,13 @@ def create_ready_domain():
|
|||
|
||||
|
||||
# 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_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
has_about_your_organization=True,
|
||||
has_anything_else=True,
|
||||
has_cisa_representative=True,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=False,
|
||||
submitter=False,
|
||||
|
@ -929,6 +930,10 @@ def completed_domain_request(
|
|||
domain_request.current_websites.add(current)
|
||||
if has_alternative_gov_domain:
|
||||
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
|
||||
|
||||
|
|
|
@ -1445,18 +1445,23 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# The results are filtered by "status in [submitted,in review,action needed]"
|
||||
self.assertContains(response, "status in [submitted,in review,action needed]", count=1)
|
||||
|
||||
def transition_state_and_send_email(self, domain_request, status, rejection_reason=None):
|
||||
@less_console_noise_decorator
|
||||
def transition_state_and_send_email(self, domain_request, status, rejection_reason=None, action_needed_reason=None):
|
||||
"""Helper method for the email test cases."""
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
|
||||
# Modify the domain request's properties
|
||||
domain_request.status = status
|
||||
|
||||
if rejection_reason:
|
||||
domain_request.rejection_reason = rejection_reason
|
||||
|
||||
if action_needed_reason:
|
||||
domain_request.action_needed_reason = action_needed_reason
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, domain_request, form=None, change=True)
|
||||
|
||||
|
@ -1493,6 +1498,57 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
bcc_email = kwargs["Destination"]["BccAddresses"][0]
|
||||
self.assertEqual(bcc_email, bcc_email_address)
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_action_needed_sends_reason_email_prod_bcc(self):
|
||||
"""When an action needed reason is set, an email is sent out and help@get.gov
|
||||
is BCC'd in production"""
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=in_review)
|
||||
|
||||
# Test the email sent out for already_has_domains
|
||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=already_has_domains)
|
||||
self.assert_email_is_accurate("ORGANIZATION ALREADY HAS A .GOV DOMAIN", 0, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test the email sent out for bad_name
|
||||
bad_name = DomainRequest.ActionNeededReasons.BAD_NAME
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=bad_name)
|
||||
self.assert_email_is_accurate(
|
||||
"DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS", 1, EMAIL, bcc_email_address=BCC_EMAIL
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
# Test the email sent out for eligibility_unclear
|
||||
eligibility_unclear = DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=eligibility_unclear)
|
||||
self.assert_email_is_accurate(
|
||||
"ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", 2, EMAIL, bcc_email_address=BCC_EMAIL
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Test the email sent out for questionable_ao
|
||||
questionable_ao = DomainRequest.ActionNeededReasons.QUESTIONABLE_AUTHORIZING_OFFICIAL
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_ao)
|
||||
self.assert_email_is_accurate(
|
||||
"AUTHORIZING OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||
|
||||
# Assert that no other emails are sent on OTHER
|
||||
other = DomainRequest.ActionNeededReasons.OTHER
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=other)
|
||||
|
||||
# Should be unchanged from before
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||
|
||||
def test_save_model_sends_submitted_email(self):
|
||||
"""When transitioning to submitted from started or withdrawn on a domain request,
|
||||
an email is sent out.
|
||||
|
@ -1528,7 +1584,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
other = DomainRequest.ActionNeededReasons.OTHER
|
||||
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
|
||||
|
@ -1536,7 +1594,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to ACTION_NEEDED
|
||||
|
@ -1586,7 +1644,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
other = domain_request.ActionNeededReasons.OTHER
|
||||
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
|
||||
|
@ -1594,7 +1654,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
self.transition_state_and_send_email(domain_request, in_review, action_needed_reason=other)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to ACTION_NEEDED
|
||||
|
@ -2238,6 +2298,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"updated_at",
|
||||
"status",
|
||||
"rejection_reason",
|
||||
"action_needed_reason",
|
||||
"federal_agency",
|
||||
"creator",
|
||||
"investigator",
|
||||
|
@ -2265,6 +2326,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"anything_else",
|
||||
"has_anything_else_text",
|
||||
"cisa_representative_email",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"has_cisa_representative",
|
||||
"is_policy_acknowledged",
|
||||
"submission_date",
|
||||
|
@ -2297,6 +2360,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"cisa_representative_first_name",
|
||||
"cisa_representative_last_name",
|
||||
"cisa_representative_email",
|
||||
]
|
||||
|
||||
|
@ -2395,6 +2460,10 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
domain_request.status = another_state
|
||||
|
||||
if another_state == DomainRequest.DomainRequestStatus.ACTION_NEEDED:
|
||||
domain_request.action_needed_reason = domain_request.ActionNeededReasons.OTHER
|
||||
|
||||
domain_request.rejection_reason = rejection_reason
|
||||
|
||||
self.admin.save_model(request, domain_request, None, True)
|
||||
|
|
|
@ -1802,93 +1802,129 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
def test_is_additional_details_complete(self):
|
||||
test_cases = [
|
||||
# CISA Rep - Yes
|
||||
# Firstname - Yes
|
||||
# Lastname - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - Yes
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_first_name": "cisa-first-name",
|
||||
"cisa_representative_last_name": "cisa-last-name",
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Firstname - Yes
|
||||
# Lastname - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_first_name": "cisa-first-name",
|
||||
"cisa_representative_last_name": "cisa-last-name",
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# 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 Text - No
|
||||
{
|
||||
"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,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - None
|
||||
# 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
|
||||
# Firstname - Yes
|
||||
# Lastname - Yes
|
||||
# Email - None
|
||||
# Anything Else Radio - None
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_first_name": "cisa-first-name",
|
||||
"cisa_representative_last_name": "cisa-last-name",
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
"expected": False,
|
||||
},
|
||||
# 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
|
||||
# Anything Else Radio - 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
|
||||
{
|
||||
"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,
|
||||
"has_anything_else_text": False,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Firstname - None
|
||||
# Lastname - None
|
||||
# Email - None
|
||||
# Anything Else Radio - Yes
|
||||
# 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,
|
||||
# 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,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Firstname - None
|
||||
# Lastname - None
|
||||
# Email - None
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - Yes
|
||||
{
|
||||
"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,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
|
@ -1899,6 +1935,8 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
# Anything Else Text - Yes
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_first_name": None,
|
||||
"cisa_representative_last_name": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
|
@ -1909,6 +1947,8 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_first_name": None,
|
||||
"cisa_representative_last_name": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
|
@ -1919,6 +1959,8 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_first_name": None,
|
||||
"cisa_representative_last_name": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
|
@ -1930,6 +1972,8 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
# Anything Else Text - No
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_first_name": None,
|
||||
"cisa_representative_last_name": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": False,
|
||||
"anything_else": None,
|
||||
|
@ -1939,6 +1983,8 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
# Anything Else Radio - None
|
||||
{
|
||||
"has_cisa_representative": None,
|
||||
"cisa_representative_first_name": None,
|
||||
"cisa_representative_last_name": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
|
|
|
@ -366,6 +366,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "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-anything_else"] = "Nothing else."
|
||||
|
||||
|
@ -374,6 +376,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
additional_details_result = additional_details_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
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.anything_else, "Nothing else.")
|
||||
# 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_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-anything_else"] = "Nothing else."
|
||||
|
||||
|
@ -727,6 +733,8 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
additional_details_result = additional_details_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
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.anything_else, "Nothing else.")
|
||||
# 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):
|
||||
"""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"""
|
||||
|
||||
domain_request = completed_domain_request(user=self.user, has_anything_else=True)
|
||||
domain_request.cisa_representative_email = "test@igorville.gov"
|
||||
domain_request = completed_domain_request(user=self.user, has_anything_else=True, has_cisa_representative=True)
|
||||
domain_request.anything_else = "1234"
|
||||
domain_request.save()
|
||||
|
||||
|
@ -1181,12 +1188,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
"""On the Additional details page, the form preselects "no" when has_cisa_representative
|
||||
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.
|
||||
# This means that we should expect this to correlate with the no button.
|
||||
domain_request.has_anything_else_text = False
|
||||
domain_request.has_cisa_representative = False
|
||||
domain_request.save()
|
||||
|
||||
# prime the form by visiting /edit
|
||||
|
@ -1205,7 +1213,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
# Check the cisa representative yes/no field
|
||||
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
|
||||
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,
|
||||
the domain request's data gets wiped when submitted"""
|
||||
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.save()
|
||||
|
||||
# Make sure we have the data we need for the test
|
||||
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")
|
||||
|
||||
# prime the form by visiting /edit
|
||||
|
@ -1253,25 +1265,31 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
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")
|
||||
|
||||
# Check that our data has been cleared
|
||||
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)
|
||||
|
||||
# Double check the yes/no fields
|
||||
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):
|
||||
"""When a user submits the Additional Details form,
|
||||
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
|
||||
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
|
||||
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
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "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-anything_else"] = "redandblue"
|
||||
|
||||
|
@ -1302,10 +1322,12 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
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")
|
||||
|
||||
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.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):
|
||||
"""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
|
||||
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.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):
|
||||
"""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):
|
||||
"""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"""
|
||||
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_cisa_representative, None)
|
||||
|
|
|
@ -310,6 +310,7 @@ def export_data_type_to_csv(csv_file):
|
|||
"""
|
||||
All domains report with extra columns.
|
||||
This maps to the "All domain metadata" button.
|
||||
Exports domains of all statuses.
|
||||
"""
|
||||
|
||||
writer = csv.writer(csv_file)
|
||||
|
@ -337,15 +338,8 @@ def export_data_type_to_csv(csv_file):
|
|||
"federal_agency",
|
||||
"domain__name",
|
||||
]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import boto3
|
||||
import logging
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
|
@ -27,6 +28,7 @@ def send_templated_email(
|
|||
bcc_address="",
|
||||
context={},
|
||||
attachment_file: str = None,
|
||||
wrap_email=False,
|
||||
):
|
||||
"""Send an email built from a template to one email address.
|
||||
|
||||
|
@ -66,6 +68,11 @@ def send_templated_email(
|
|||
|
||||
try:
|
||||
if attachment_file is None:
|
||||
# Wrap the email body to a maximum width of 80 characters per line.
|
||||
# Not all email clients support CSS to do this, and our .txt files require parsing.
|
||||
if wrap_email:
|
||||
email_body = wrap_text_and_preserve_paragraphs(email_body, width=80)
|
||||
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination=destination,
|
||||
|
@ -91,6 +98,26 @@ def send_templated_email(
|
|||
raise EmailSendingError("Could not send SES email.") from exc
|
||||
|
||||
|
||||
def wrap_text_and_preserve_paragraphs(text, width):
|
||||
"""
|
||||
Wraps text to `width` preserving newlines; splits on '\n', wraps segments, rejoins with '\n'.
|
||||
Args:
|
||||
text (str): Text to wrap.
|
||||
width (int): Max width per line, default 80.
|
||||
|
||||
Returns:
|
||||
str: Wrapped text with preserved paragraph structure.
|
||||
"""
|
||||
# Split text into paragraphs by newlines
|
||||
paragraphs = text.split("\n")
|
||||
|
||||
# Add \n to any line that exceeds our max length
|
||||
wrapped_paragraphs = [textwrap.fill(paragraph, width=width) for paragraph in paragraphs]
|
||||
|
||||
# Join paragraphs with double newlines
|
||||
return "\n".join(wrapped_paragraphs)
|
||||
|
||||
|
||||
def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client):
|
||||
# Create a multipart/mixed parent container
|
||||
msg = MIMEMultipart("mixed")
|
||||
|
|
|
@ -79,6 +79,7 @@ class FSMErrorCodes(IntEnum):
|
|||
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
||||
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
|
||||
- 5 NO_REJECTION_REASON No rejection reason is specified
|
||||
- 6 NO_ACTION_NEEDED_REASON No action needed reason is specified
|
||||
"""
|
||||
|
||||
APPROVE_DOMAIN_IN_USE = 1
|
||||
|
@ -86,6 +87,7 @@ class FSMErrorCodes(IntEnum):
|
|||
INVESTIGATOR_NOT_STAFF = 3
|
||||
INVESTIGATOR_NOT_SUBMITTER = 4
|
||||
NO_REJECTION_REASON = 5
|
||||
NO_ACTION_NEEDED_REASON = 6
|
||||
|
||||
|
||||
class FSMDomainRequestError(Exception):
|
||||
|
@ -100,6 +102,7 @@ class FSMDomainRequestError(Exception):
|
|||
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
|
||||
FSMErrorCodes.NO_REJECTION_REASON: ("A rejection reason is required."),
|
||||
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
|
|
|
@ -369,7 +369,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
or self.domain_request.no_other_contacts_rationale is not None
|
||||
),
|
||||
"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
|
||||
),
|
||||
"requirements": self.domain_request.is_policy_acknowledged is not None,
|
||||
|
@ -380,7 +380,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
def get_context_data(self):
|
||||
"""Define context for access on all wizard pages."""
|
||||
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
||||
logger.debug("PROFILE FLAG is %s" % has_profile_flag)
|
||||
|
||||
context_stuff = {}
|
||||
if DomainRequest._form_complete(self.domain_request):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue