mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-11 06:54:48 +02:00
Merge branch 'main' into za/1889-socket-in-use-error
This commit is contained in:
commit
f2c0b25eff
13 changed files with 781 additions and 875 deletions
|
@ -16,6 +16,7 @@ from dateutil.relativedelta import relativedelta # type: ignore
|
|||
from epplibwrapper.errors import ErrorCode, RegistryError
|
||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||
from django.contrib.admin.views.main import ORDER_VAR
|
||||
from registrar.widgets import NoAutocompleteFilteredSelectMultiple
|
||||
|
@ -92,9 +93,14 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
# first option in status transitions is current state
|
||||
available_transitions = [(current_state, domain_request.get_status_display())]
|
||||
|
||||
transitions = get_available_FIELD_transitions(
|
||||
domain_request, models.DomainRequest._meta.get_field("status")
|
||||
)
|
||||
if domain_request.investigator is not None:
|
||||
transitions = get_available_FIELD_transitions(
|
||||
domain_request, models.DomainRequest._meta.get_field("status")
|
||||
)
|
||||
else:
|
||||
transitions = self.get_custom_field_transitions(
|
||||
domain_request, models.DomainRequest._meta.get_field("status")
|
||||
)
|
||||
|
||||
for transition in transitions:
|
||||
available_transitions.append((transition.target, transition.target.label))
|
||||
|
@ -105,6 +111,73 @@ class DomainRequestAdminForm(forms.ModelForm):
|
|||
if not domain_request.creator.is_restricted():
|
||||
self.fields["status"].widget.choices = available_transitions
|
||||
|
||||
def get_custom_field_transitions(self, instance, field):
|
||||
"""Custom implementation of get_available_FIELD_transitions
|
||||
in the FSM. Allows us to still display fields filtered out by a condition."""
|
||||
curr_state = field.get_state(instance)
|
||||
transitions = field.transitions[instance.__class__]
|
||||
|
||||
for name, transition in transitions.items():
|
||||
meta = transition._django_fsm
|
||||
if meta.has_transition(curr_state):
|
||||
yield meta.get_transition(curr_state)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Override of the default clean on the form.
|
||||
This is so we can inject custom form-level error messages.
|
||||
"""
|
||||
# clean is called from clean_forms, which is called from is_valid
|
||||
# after clean_fields. it is used to determine form level errors.
|
||||
# is_valid is typically called from view during a post
|
||||
cleaned_data = super().clean()
|
||||
status = cleaned_data.get("status")
|
||||
investigator = cleaned_data.get("investigator")
|
||||
|
||||
# Get the old status
|
||||
initial_status = self.initial.get("status", None)
|
||||
|
||||
# We only care about investigator when in these statuses
|
||||
checked_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||
]
|
||||
|
||||
# If a status change occured, check for validity
|
||||
if status != initial_status and status in checked_statuses:
|
||||
# Checks the "investigators" field for validity.
|
||||
# That field must obey certain conditions when an domain request is approved.
|
||||
# Will call "add_error" if any issues are found.
|
||||
self._check_for_valid_investigator(investigator)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def _check_for_valid_investigator(self, investigator) -> bool:
|
||||
"""
|
||||
Checks if the investigator field is not none, and is staff.
|
||||
Adds form errors on failure.
|
||||
"""
|
||||
|
||||
is_valid = False
|
||||
|
||||
# Check if an investigator is assigned. No approval is possible without one.
|
||||
error_message = None
|
||||
if investigator is None:
|
||||
# Lets grab the error message from a common location
|
||||
error_message = FSMApplicationError.get_error_message(FSMErrorCodes.NO_INVESTIGATOR)
|
||||
elif not investigator.is_staff:
|
||||
error_message = FSMApplicationError.get_error_message(FSMErrorCodes.INVESTIGATOR_NOT_STAFF)
|
||||
else:
|
||||
is_valid = True
|
||||
|
||||
if error_message is not None:
|
||||
self.add_error("investigator", error_message)
|
||||
|
||||
return is_valid
|
||||
|
||||
|
||||
# Based off of this excellent example: https://djangosnippets.org/snippets/10471/
|
||||
class MultiFieldSortableChangeList(admin.views.main.ChangeList):
|
||||
|
@ -1054,72 +1127,24 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
|
||||
# Trigger action when a fieldset is changed
|
||||
def save_model(self, request, obj, form, change):
|
||||
if obj and obj.creator.status != models.User.RESTRICTED:
|
||||
if change: # Check if the domain request is being edited
|
||||
# Get the original domain request from the database
|
||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||
"""Custom save_model definition that handles edge cases"""
|
||||
|
||||
if (
|
||||
obj
|
||||
and original_obj.status == models.DomainRequest.DomainRequestStatus.APPROVED
|
||||
and obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||
and not obj.domain_is_not_active()
|
||||
):
|
||||
# If an admin tried to set an approved domain request to
|
||||
# another status and the related domain is already
|
||||
# active, shortcut the action and throw a friendly
|
||||
# error message. This action would still not go through
|
||||
# shortcut or not as the rules are duplicated on the model,
|
||||
# but the error would be an ugly Django error screen.
|
||||
# == Check that the obj is in a valid state == #
|
||||
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
# If obj is none, something went very wrong.
|
||||
# The form should have blocked this, so lets forbid it.
|
||||
if not obj:
|
||||
logger.error(f"Invalid value for obj ({obj})")
|
||||
messages.set_level(request, messages.ERROR)
|
||||
messages.error(
|
||||
request,
|
||||
"Could not save DomainRequest. Something went wrong.",
|
||||
)
|
||||
return None
|
||||
|
||||
messages.error(
|
||||
request,
|
||||
"This action is not permitted. The domain is already active.",
|
||||
)
|
||||
|
||||
elif (
|
||||
obj and obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason
|
||||
):
|
||||
# This condition should never be triggered.
|
||||
# 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.
|
||||
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
|
||||
messages.error(
|
||||
request,
|
||||
"A rejection reason is required.",
|
||||
)
|
||||
|
||||
else:
|
||||
if obj.status != original_obj.status:
|
||||
status_method_mapping = {
|
||||
models.DomainRequest.DomainRequestStatus.STARTED: None,
|
||||
models.DomainRequest.DomainRequestStatus.SUBMITTED: obj.submit,
|
||||
models.DomainRequest.DomainRequestStatus.IN_REVIEW: obj.in_review,
|
||||
models.DomainRequest.DomainRequestStatus.ACTION_NEEDED: obj.action_needed,
|
||||
models.DomainRequest.DomainRequestStatus.APPROVED: obj.approve,
|
||||
models.DomainRequest.DomainRequestStatus.WITHDRAWN: obj.withdraw,
|
||||
models.DomainRequest.DomainRequestStatus.REJECTED: obj.reject,
|
||||
models.DomainRequest.DomainRequestStatus.INELIGIBLE: (obj.reject_with_prejudice),
|
||||
}
|
||||
selected_method = status_method_mapping.get(obj.status)
|
||||
if selected_method is None:
|
||||
logger.warning("Unknown status selected in django admin")
|
||||
else:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we roll back the
|
||||
# status to what it was before the admin user changed it and
|
||||
# let the fsm method set it.
|
||||
obj.status = original_obj.status
|
||||
selected_method()
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
else:
|
||||
# If the user is restricted or we're saving an invalid model,
|
||||
# forbid this action.
|
||||
if not obj or obj.creator.status == models.User.RESTRICTED:
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
|
||||
|
@ -1128,6 +1153,117 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
"This action is not permitted for domain requests with a restricted creator.",
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# == Check if we're making a change or not == #
|
||||
|
||||
# If we're not making a change (adding a record), run save model as we do normally
|
||||
if not change:
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle non-status changes == #
|
||||
|
||||
# Get the original domain request from the database.
|
||||
original_obj = models.DomainRequest.objects.get(pk=obj.pk)
|
||||
if obj.status == original_obj.status:
|
||||
# If the status hasn't changed, let the base function take care of it
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
# == Handle status changes == #
|
||||
|
||||
# Run some checks on the current object for invalid status changes
|
||||
obj, should_save = self._handle_status_change(request, obj, original_obj)
|
||||
|
||||
# We should only save if we don't display any errors in the step above.
|
||||
if should_save:
|
||||
return super().save_model(request, obj, form, change)
|
||||
|
||||
def _handle_status_change(self, request, obj, original_obj):
|
||||
"""
|
||||
Checks for various conditions when a status change is triggered.
|
||||
In the event that it is valid, the status will be mapped to
|
||||
the appropriate method.
|
||||
|
||||
In the event that we should not status change, an error message
|
||||
will be displayed.
|
||||
|
||||
Returns a tuple: (obj: DomainRequest, should_proceed: bool)
|
||||
"""
|
||||
|
||||
should_proceed = True
|
||||
error_message = None
|
||||
|
||||
# Get the method that should be run given the status
|
||||
selected_method = self.get_status_method_mapping(obj)
|
||||
if selected_method is None:
|
||||
logger.warning("Unknown status selected in django admin")
|
||||
|
||||
# If the status is not mapped properly, saving could cause
|
||||
# weird issues down the line. Instead, we should block this.
|
||||
should_proceed = False
|
||||
return should_proceed
|
||||
|
||||
request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||
if request_is_not_approved and not obj.domain_is_not_active():
|
||||
# If an admin tried to set an approved domain request to
|
||||
# another status and the related domain is already
|
||||
# active, shortcut the action and throw a friendly
|
||||
# error message. This action would still not go through
|
||||
# shortcut or not as the rules are duplicated on the model,
|
||||
# but the error would be an ugly Django error screen.
|
||||
error_message = "This action is not permitted. The domain is already active."
|
||||
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
||||
# This condition should never be triggered.
|
||||
# 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 = "A rejection reason is required."
|
||||
else:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we roll back the
|
||||
# status to what it was before the admin user changed it and
|
||||
# let the fsm method set it.
|
||||
obj.status = original_obj.status
|
||||
|
||||
# Try to perform the status change.
|
||||
# Catch FSMApplicationError's and return the message,
|
||||
# as these are typically user errors.
|
||||
try:
|
||||
selected_method()
|
||||
except FSMApplicationError as err:
|
||||
logger.warning(f"An error encountered when trying to change status: {err}")
|
||||
error_message = err.message
|
||||
|
||||
if error_message is not None:
|
||||
# Clear the success message
|
||||
messages.set_level(request, messages.ERROR)
|
||||
# Display the error
|
||||
messages.error(
|
||||
request,
|
||||
error_message,
|
||||
)
|
||||
|
||||
# If an error message exists, we shouldn't proceed
|
||||
should_proceed = False
|
||||
|
||||
return (obj, should_proceed)
|
||||
|
||||
def get_status_method_mapping(self, domain_request):
|
||||
"""Returns what method should be ran given an domain request object"""
|
||||
# Define a per-object mapping
|
||||
status_method_mapping = {
|
||||
models.DomainRequest.DomainRequestStatus.STARTED: None,
|
||||
models.DomainRequest.DomainRequestStatus.SUBMITTED: domain_request.submit,
|
||||
models.DomainRequest.DomainRequestStatus.IN_REVIEW: domain_request.in_review,
|
||||
models.DomainRequest.DomainRequestStatus.ACTION_NEEDED: domain_request.action_needed,
|
||||
models.DomainRequest.DomainRequestStatus.APPROVED: domain_request.approve,
|
||||
models.DomainRequest.DomainRequestStatus.WITHDRAWN: domain_request.withdraw,
|
||||
models.DomainRequest.DomainRequestStatus.REJECTED: domain_request.reject,
|
||||
models.DomainRequest.DomainRequestStatus.INELIGIBLE: (domain_request.reject_with_prejudice),
|
||||
}
|
||||
|
||||
# Grab the method
|
||||
return status_method_mapping.get(domain_request.status, None)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 2 conditions that determine which fields are read-only:
|
||||
|
|
|
@ -277,11 +277,6 @@ h1, h2, h3,
|
|||
}
|
||||
}
|
||||
|
||||
// Hides the "clear" button on autocomplete, as we already have one to use
|
||||
.select2-selection__clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Fixes a display issue where the list was entirely white, or had too much whitespace
|
||||
.select2-dropdown {
|
||||
display: inline-grid !important;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
|
@ -184,6 +185,14 @@ class DomainRequestFixture:
|
|||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
cls._create_domain_requests(users)
|
||||
|
||||
@classmethod
|
||||
def _create_domain_requests(cls, users):
|
||||
"""Creates DomainRequests given a list of users"""
|
||||
for user in users:
|
||||
logger.debug("Loading domain requests for %s" % user)
|
||||
for app in cls.DA:
|
||||
|
@ -211,8 +220,16 @@ class DomainFixture(DomainRequestFixture):
|
|||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
# approve each user associated with `in review` status domains
|
||||
DomainFixture._approve_domain_requests(users)
|
||||
|
||||
@staticmethod
|
||||
def _approve_domain_requests(users):
|
||||
"""Approves all provided domain requests if they are in the state in_review"""
|
||||
for user in users:
|
||||
# approve one of each users in review status domains
|
||||
domain_request = DomainRequest.objects.filter(
|
||||
creator=user, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
).last()
|
||||
|
@ -220,5 +237,13 @@ class DomainFixture(DomainRequestFixture):
|
|||
|
||||
# We don't want fixtures sending out real emails to
|
||||
# fake email addresses, so we just skip that and log it instead
|
||||
|
||||
# All approvals require an investigator, so if there is none,
|
||||
# assign one.
|
||||
if domain_request.investigator is None:
|
||||
# All "users" in fixtures have admin perms per prior config.
|
||||
# No need to check for that.
|
||||
domain_request.investigator = random.choice(users) # nosec
|
||||
|
||||
domain_request.approve(send_email=False)
|
||||
domain_request.save()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import (
|
||||
User,
|
||||
|
@ -186,5 +187,12 @@ class UserFixture:
|
|||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.load_users(cls, cls.ADMINS, "full_access_group")
|
||||
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
# This is slightly different then bulk_create or bulk_update, in that
|
||||
# you still get the same behaviour of .save(), but those incremental
|
||||
# steps now do not need to close/reopen a db connection,
|
||||
# instead they share one.
|
||||
with transaction.atomic():
|
||||
cls.load_users(cls, cls.ADMINS, "full_access_group")
|
||||
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-12 16:50
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0072_alter_publiccontact_fax_alter_publiccontact_voice"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="approved_domain",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The approved domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_request",
|
||||
to="registrar.domain",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="creator",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_requests_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="investigator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_requests_investigating",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="contact_domain_requests", to="registrar.contact", verbose_name="contacts"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="requested_domain",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The requested domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_request",
|
||||
to="registrar.draftdomain",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="domain_application",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="Associated domain request",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="DomainRequest_info",
|
||||
to="registrar.domainapplication",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,685 +0,0 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-07 21:52
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django_fsm
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0072_alter_publiccontact_fax_alter_publiccontact_voice"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DomainRequest",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"status",
|
||||
django_fsm.FSMField(
|
||||
choices=[
|
||||
("started", "Started"),
|
||||
("submitted", "Submitted"),
|
||||
("in review", "In review"),
|
||||
("action needed", "Action needed"),
|
||||
("approved", "Approved"),
|
||||
("withdrawn", "Withdrawn"),
|
||||
("rejected", "Rejected"),
|
||||
("ineligible", "Ineligible"),
|
||||
],
|
||||
default="started",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
(
|
||||
"rejection_reason",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("purpose_not_met", "Purpose requirements not met"),
|
||||
("requestor_not_eligible", "Requestor not eligible to make request"),
|
||||
("org_has_domain", "Org already has a .gov domain"),
|
||||
("contacts_not_verified", "Org contacts couldn't be verified"),
|
||||
("org_not_eligible", "Org not eligible for a .gov domain"),
|
||||
("naming_not_met", "Naming requirements not met"),
|
||||
("other", "Other/Unspecified"),
|
||||
],
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"organization_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
help_text="Type of organization",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"federally_recognized_tribe",
|
||||
models.BooleanField(help_text="Is the tribe federally recognized", null=True),
|
||||
),
|
||||
(
|
||||
"state_recognized_tribe",
|
||||
models.BooleanField(help_text="Is the tribe recognized by a state", null=True),
|
||||
),
|
||||
("tribe_name", models.CharField(blank=True, help_text="Name of tribe", null=True)),
|
||||
(
|
||||
"federal_agency",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(
|
||||
"Administrative Conference of the United States",
|
||||
"Administrative Conference of the United States",
|
||||
),
|
||||
("Advisory Council on Historic Preservation", "Advisory Council on Historic Preservation"),
|
||||
("American Battle Monuments Commission", "American Battle Monuments Commission"),
|
||||
("AMTRAK", "AMTRAK"),
|
||||
("Appalachian Regional Commission", "Appalachian Regional Commission"),
|
||||
(
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
"Appraisal Subcommittee of the Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Appraisal Subcommittee", "Appraisal Subcommittee"),
|
||||
("Architect of the Capitol", "Architect of the Capitol"),
|
||||
("Armed Forces Retirement Home", "Armed Forces Retirement Home"),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Foundation",
|
||||
),
|
||||
(
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
"Barry Goldwater Scholarship and Excellence in Education Program",
|
||||
),
|
||||
("Central Intelligence Agency", "Central Intelligence Agency"),
|
||||
("Chemical Safety Board", "Chemical Safety Board"),
|
||||
(
|
||||
"Christopher Columbus Fellowship Foundation",
|
||||
"Christopher Columbus Fellowship Foundation",
|
||||
),
|
||||
(
|
||||
"Civil Rights Cold Case Records Review Board",
|
||||
"Civil Rights Cold Case Records Review Board",
|
||||
),
|
||||
(
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
"Commission for the Preservation of America's Heritage Abroad",
|
||||
),
|
||||
("Commission of Fine Arts", "Commission of Fine Arts"),
|
||||
(
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
"Committee for Purchase From People Who Are Blind or Severely Disabled",
|
||||
),
|
||||
("Commodity Futures Trading Commission", "Commodity Futures Trading Commission"),
|
||||
("Congressional Budget Office", "Congressional Budget Office"),
|
||||
("Consumer Financial Protection Bureau", "Consumer Financial Protection Bureau"),
|
||||
("Consumer Product Safety Commission", "Consumer Product Safety Commission"),
|
||||
(
|
||||
"Corporation for National & Community Service",
|
||||
"Corporation for National & Community Service",
|
||||
),
|
||||
(
|
||||
"Corporation for National and Community Service",
|
||||
"Corporation for National and Community Service",
|
||||
),
|
||||
(
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
"Council of Inspectors General on Integrity and Efficiency",
|
||||
),
|
||||
("Court Services and Offender Supervision", "Court Services and Offender Supervision"),
|
||||
("Cyberspace Solarium Commission", "Cyberspace Solarium Commission"),
|
||||
(
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
"DC Court Services and Offender Supervision Agency",
|
||||
),
|
||||
("DC Pre-trial Services", "DC Pre-trial Services"),
|
||||
("Defense Nuclear Facilities Safety Board", "Defense Nuclear Facilities Safety Board"),
|
||||
("Delta Regional Authority", "Delta Regional Authority"),
|
||||
("Denali Commission", "Denali Commission"),
|
||||
("Department of Agriculture", "Department of Agriculture"),
|
||||
("Department of Commerce", "Department of Commerce"),
|
||||
("Department of Defense", "Department of Defense"),
|
||||
("Department of Education", "Department of Education"),
|
||||
("Department of Energy", "Department of Energy"),
|
||||
("Department of Health and Human Services", "Department of Health and Human Services"),
|
||||
("Department of Homeland Security", "Department of Homeland Security"),
|
||||
(
|
||||
"Department of Housing and Urban Development",
|
||||
"Department of Housing and Urban Development",
|
||||
),
|
||||
("Department of Justice", "Department of Justice"),
|
||||
("Department of Labor", "Department of Labor"),
|
||||
("Department of State", "Department of State"),
|
||||
("Department of the Interior", "Department of the Interior"),
|
||||
("Department of the Treasury", "Department of the Treasury"),
|
||||
("Department of Transportation", "Department of Transportation"),
|
||||
("Department of Veterans Affairs", "Department of Veterans Affairs"),
|
||||
("Director of National Intelligence", "Director of National Intelligence"),
|
||||
("Dwight D. Eisenhower Memorial Commission", "Dwight D. Eisenhower Memorial Commission"),
|
||||
("Election Assistance Commission", "Election Assistance Commission"),
|
||||
("Environmental Protection Agency", "Environmental Protection Agency"),
|
||||
("Equal Employment Opportunity Commission", "Equal Employment Opportunity Commission"),
|
||||
("Executive Office of the President", "Executive Office of the President"),
|
||||
("Export-Import Bank of the United States", "Export-Import Bank of the United States"),
|
||||
("Export/Import Bank of the U.S.", "Export/Import Bank of the U.S."),
|
||||
("Farm Credit Administration", "Farm Credit Administration"),
|
||||
("Farm Credit System Insurance Corporation", "Farm Credit System Insurance Corporation"),
|
||||
("Federal Communications Commission", "Federal Communications Commission"),
|
||||
("Federal Deposit Insurance Corporation", "Federal Deposit Insurance Corporation"),
|
||||
("Federal Election Commission", "Federal Election Commission"),
|
||||
("Federal Energy Regulatory Commission", "Federal Energy Regulatory Commission"),
|
||||
(
|
||||
"Federal Financial Institutions Examination Council",
|
||||
"Federal Financial Institutions Examination Council",
|
||||
),
|
||||
("Federal Housing Finance Agency", "Federal Housing Finance Agency"),
|
||||
("Federal Judiciary", "Federal Judiciary"),
|
||||
("Federal Labor Relations Authority", "Federal Labor Relations Authority"),
|
||||
("Federal Maritime Commission", "Federal Maritime Commission"),
|
||||
(
|
||||
"Federal Mediation and Conciliation Service",
|
||||
"Federal Mediation and Conciliation Service",
|
||||
),
|
||||
(
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
"Federal Mine Safety and Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
"Federal Permitting Improvement Steering Council",
|
||||
),
|
||||
("Federal Reserve Board of Governors", "Federal Reserve Board of Governors"),
|
||||
("Federal Reserve System", "Federal Reserve System"),
|
||||
("Federal Trade Commission", "Federal Trade Commission"),
|
||||
("General Services Administration", "General Services Administration"),
|
||||
("gov Administration", "gov Administration"),
|
||||
("Government Accountability Office", "Government Accountability Office"),
|
||||
("Government Publishing Office", "Government Publishing Office"),
|
||||
("Gulf Coast Ecosystem Restoration Council", "Gulf Coast Ecosystem Restoration Council"),
|
||||
("Harry S Truman Scholarship Foundation", "Harry S Truman Scholarship Foundation"),
|
||||
("Harry S. Truman Scholarship Foundation", "Harry S. Truman Scholarship Foundation"),
|
||||
("Institute of Museum and Library Services", "Institute of Museum and Library Services"),
|
||||
("Institute of Peace", "Institute of Peace"),
|
||||
("Inter-American Foundation", "Inter-American Foundation"),
|
||||
(
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
"International Boundary and Water Commission: United States and Mexico",
|
||||
),
|
||||
(
|
||||
"International Boundary Commission: United States and Canada",
|
||||
"International Boundary Commission: United States and Canada",
|
||||
),
|
||||
(
|
||||
"International Joint Commission: United States and Canada",
|
||||
"International Joint Commission: United States and Canada",
|
||||
),
|
||||
(
|
||||
"James Madison Memorial Fellowship Foundation",
|
||||
"James Madison Memorial Fellowship Foundation",
|
||||
),
|
||||
("Japan-United States Friendship Commission", "Japan-United States Friendship Commission"),
|
||||
("Japan-US Friendship Commission", "Japan-US Friendship Commission"),
|
||||
(
|
||||
"John F. Kennedy Center for Performing Arts",
|
||||
"John F. Kennedy Center for Performing Arts",
|
||||
),
|
||||
(
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
"John F. Kennedy Center for the Performing Arts",
|
||||
),
|
||||
("Legal Services Corporation", "Legal Services Corporation"),
|
||||
("Legislative Branch", "Legislative Branch"),
|
||||
("Library of Congress", "Library of Congress"),
|
||||
("Marine Mammal Commission", "Marine Mammal Commission"),
|
||||
(
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
"Medicaid and CHIP Payment and Access Commission",
|
||||
),
|
||||
("Medical Payment Advisory Commission", "Medical Payment Advisory Commission"),
|
||||
("Medicare Payment Advisory Commission", "Medicare Payment Advisory Commission"),
|
||||
("Merit Systems Protection Board", "Merit Systems Protection Board"),
|
||||
("Millennium Challenge Corporation", "Millennium Challenge Corporation"),
|
||||
(
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
"Morris K. Udall and Stewart L. Udall Foundation",
|
||||
),
|
||||
(
|
||||
"National Aeronautics and Space Administration",
|
||||
"National Aeronautics and Space Administration",
|
||||
),
|
||||
(
|
||||
"National Archives and Records Administration",
|
||||
"National Archives and Records Administration",
|
||||
),
|
||||
("National Capital Planning Commission", "National Capital Planning Commission"),
|
||||
("National Council on Disability", "National Council on Disability"),
|
||||
("National Credit Union Administration", "National Credit Union Administration"),
|
||||
("National Endowment for the Arts", "National Endowment for the Arts"),
|
||||
("National Endowment for the Humanities", "National Endowment for the Humanities"),
|
||||
(
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
"National Foundation on the Arts and the Humanities",
|
||||
),
|
||||
("National Gallery of Art", "National Gallery of Art"),
|
||||
("National Indian Gaming Commission", "National Indian Gaming Commission"),
|
||||
("National Labor Relations Board", "National Labor Relations Board"),
|
||||
("National Mediation Board", "National Mediation Board"),
|
||||
("National Science Foundation", "National Science Foundation"),
|
||||
(
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
"National Security Commission on Artificial Intelligence",
|
||||
),
|
||||
("National Transportation Safety Board", "National Transportation Safety Board"),
|
||||
(
|
||||
"Networking Information Technology Research and Development",
|
||||
"Networking Information Technology Research and Development",
|
||||
),
|
||||
("Non-Federal Agency", "Non-Federal Agency"),
|
||||
("Northern Border Regional Commission", "Northern Border Regional Commission"),
|
||||
("Nuclear Regulatory Commission", "Nuclear Regulatory Commission"),
|
||||
("Nuclear Safety Oversight Committee", "Nuclear Safety Oversight Committee"),
|
||||
("Nuclear Waste Technical Review Board", "Nuclear Waste Technical Review Board"),
|
||||
(
|
||||
"Occupational Safety & Health Review Commission",
|
||||
"Occupational Safety & Health Review Commission",
|
||||
),
|
||||
(
|
||||
"Occupational Safety and Health Review Commission",
|
||||
"Occupational Safety and Health Review Commission",
|
||||
),
|
||||
("Office of Compliance", "Office of Compliance"),
|
||||
("Office of Congressional Workplace Rights", "Office of Congressional Workplace Rights"),
|
||||
("Office of Government Ethics", "Office of Government Ethics"),
|
||||
(
|
||||
"Office of Navajo and Hopi Indian Relocation",
|
||||
"Office of Navajo and Hopi Indian Relocation",
|
||||
),
|
||||
("Office of Personnel Management", "Office of Personnel Management"),
|
||||
("Open World Leadership Center", "Open World Leadership Center"),
|
||||
("Overseas Private Investment Corporation", "Overseas Private Investment Corporation"),
|
||||
("Peace Corps", "Peace Corps"),
|
||||
("Pension Benefit Guaranty Corporation", "Pension Benefit Guaranty Corporation"),
|
||||
("Postal Regulatory Commission", "Postal Regulatory Commission"),
|
||||
("Presidio Trust", "Presidio Trust"),
|
||||
(
|
||||
"Privacy and Civil Liberties Oversight Board",
|
||||
"Privacy and Civil Liberties Oversight Board",
|
||||
),
|
||||
("Public Buildings Reform Board", "Public Buildings Reform Board"),
|
||||
(
|
||||
"Public Defender Service for the District of Columbia",
|
||||
"Public Defender Service for the District of Columbia",
|
||||
),
|
||||
("Railroad Retirement Board", "Railroad Retirement Board"),
|
||||
("Securities and Exchange Commission", "Securities and Exchange Commission"),
|
||||
("Selective Service System", "Selective Service System"),
|
||||
("Small Business Administration", "Small Business Administration"),
|
||||
("Smithsonian Institution", "Smithsonian Institution"),
|
||||
("Social Security Administration", "Social Security Administration"),
|
||||
("Social Security Advisory Board", "Social Security Advisory Board"),
|
||||
("Southeast Crescent Regional Commission", "Southeast Crescent Regional Commission"),
|
||||
("Southwest Border Regional Commission", "Southwest Border Regional Commission"),
|
||||
("State Justice Institute", "State Justice Institute"),
|
||||
("State, Local, and Tribal Government", "State, Local, and Tribal Government"),
|
||||
("Stennis Center for Public Service", "Stennis Center for Public Service"),
|
||||
("Surface Transportation Board", "Surface Transportation Board"),
|
||||
("Tennessee Valley Authority", "Tennessee Valley Authority"),
|
||||
("The Executive Office of the President", "The Executive Office of the President"),
|
||||
("The Intelligence Community", "The Intelligence Community"),
|
||||
("The Legislative Branch", "The Legislative Branch"),
|
||||
("The Supreme Court", "The Supreme Court"),
|
||||
(
|
||||
"The United States World War One Centennial Commission",
|
||||
"The United States World War One Centennial Commission",
|
||||
),
|
||||
("U.S. Access Board", "U.S. Access Board"),
|
||||
("U.S. Agency for Global Media", "U.S. Agency for Global Media"),
|
||||
("U.S. Agency for International Development", "U.S. Agency for International Development"),
|
||||
("U.S. Capitol Police", "U.S. Capitol Police"),
|
||||
("U.S. Chemical Safety Board", "U.S. Chemical Safety Board"),
|
||||
(
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
"U.S. China Economic and Security Review Commission",
|
||||
),
|
||||
(
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
"U.S. Commission for the Preservation of Americas Heritage Abroad",
|
||||
),
|
||||
("U.S. Commission of Fine Arts", "U.S. Commission of Fine Arts"),
|
||||
("U.S. Commission on Civil Rights", "U.S. Commission on Civil Rights"),
|
||||
(
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
"U.S. Commission on International Religious Freedom",
|
||||
),
|
||||
("U.S. Courts", "U.S. Courts"),
|
||||
("U.S. Department of Agriculture", "U.S. Department of Agriculture"),
|
||||
("U.S. Interagency Council on Homelessness", "U.S. Interagency Council on Homelessness"),
|
||||
("U.S. International Trade Commission", "U.S. International Trade Commission"),
|
||||
("U.S. Nuclear Waste Technical Review Board", "U.S. Nuclear Waste Technical Review Board"),
|
||||
("U.S. Office of Special Counsel", "U.S. Office of Special Counsel"),
|
||||
("U.S. Peace Corps", "U.S. Peace Corps"),
|
||||
("U.S. Postal Service", "U.S. Postal Service"),
|
||||
("U.S. Semiquincentennial Commission", "U.S. Semiquincentennial Commission"),
|
||||
("U.S. Trade and Development Agency", "U.S. Trade and Development Agency"),
|
||||
(
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
"U.S.-China Economic and Security Review Commission",
|
||||
),
|
||||
("Udall Foundation", "Udall Foundation"),
|
||||
("United States AbilityOne", "United States AbilityOne"),
|
||||
("United States Access Board", "United States Access Board"),
|
||||
(
|
||||
"United States African Development Foundation",
|
||||
"United States African Development Foundation",
|
||||
),
|
||||
("United States Agency for Global Media", "United States Agency for Global Media"),
|
||||
("United States Arctic Research Commission", "United States Arctic Research Commission"),
|
||||
(
|
||||
"United States Global Change Research Program",
|
||||
"United States Global Change Research Program",
|
||||
),
|
||||
("United States Holocaust Memorial Museum", "United States Holocaust Memorial Museum"),
|
||||
("United States Institute of Peace", "United States Institute of Peace"),
|
||||
(
|
||||
"United States Interagency Council on Homelessness",
|
||||
"United States Interagency Council on Homelessness",
|
||||
),
|
||||
(
|
||||
"United States International Development Finance Corporation",
|
||||
"United States International Development Finance Corporation",
|
||||
),
|
||||
(
|
||||
"United States International Trade Commission",
|
||||
"United States International Trade Commission",
|
||||
),
|
||||
("United States Postal Service", "United States Postal Service"),
|
||||
("United States Senate", "United States Senate"),
|
||||
(
|
||||
"United States Trade and Development Agency",
|
||||
"United States Trade and Development Agency",
|
||||
),
|
||||
(
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
"Utah Reclamation Mitigation and Conservation Commission",
|
||||
),
|
||||
("Vietnam Education Foundation", "Vietnam Education Foundation"),
|
||||
("Western Hemisphere Drug Policy Commission", "Western Hemisphere Drug Policy Commission"),
|
||||
(
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
"Woodrow Wilson International Center for Scholars",
|
||||
),
|
||||
("World War I Centennial Commission", "World War I Centennial Commission"),
|
||||
],
|
||||
help_text="Federal agency",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"federal_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")],
|
||||
help_text="Federal government branch",
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_election_board",
|
||||
models.BooleanField(blank=True, help_text="Is your organization an election office?", null=True),
|
||||
),
|
||||
(
|
||||
"organization_name",
|
||||
models.CharField(blank=True, db_index=True, help_text="Organization name", null=True),
|
||||
),
|
||||
(
|
||||
"address_line1",
|
||||
models.CharField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"),
|
||||
),
|
||||
(
|
||||
"address_line2",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Street address line 2 (optional)",
|
||||
null=True,
|
||||
verbose_name="Address line 2",
|
||||
),
|
||||
),
|
||||
("city", models.CharField(blank=True, help_text="City", null=True)),
|
||||
(
|
||||
"state_territory",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("AL", "Alabama (AL)"),
|
||||
("AK", "Alaska (AK)"),
|
||||
("AS", "American Samoa (AS)"),
|
||||
("AZ", "Arizona (AZ)"),
|
||||
("AR", "Arkansas (AR)"),
|
||||
("CA", "California (CA)"),
|
||||
("CO", "Colorado (CO)"),
|
||||
("CT", "Connecticut (CT)"),
|
||||
("DE", "Delaware (DE)"),
|
||||
("DC", "District of Columbia (DC)"),
|
||||
("FL", "Florida (FL)"),
|
||||
("GA", "Georgia (GA)"),
|
||||
("GU", "Guam (GU)"),
|
||||
("HI", "Hawaii (HI)"),
|
||||
("ID", "Idaho (ID)"),
|
||||
("IL", "Illinois (IL)"),
|
||||
("IN", "Indiana (IN)"),
|
||||
("IA", "Iowa (IA)"),
|
||||
("KS", "Kansas (KS)"),
|
||||
("KY", "Kentucky (KY)"),
|
||||
("LA", "Louisiana (LA)"),
|
||||
("ME", "Maine (ME)"),
|
||||
("MD", "Maryland (MD)"),
|
||||
("MA", "Massachusetts (MA)"),
|
||||
("MI", "Michigan (MI)"),
|
||||
("MN", "Minnesota (MN)"),
|
||||
("MS", "Mississippi (MS)"),
|
||||
("MO", "Missouri (MO)"),
|
||||
("MT", "Montana (MT)"),
|
||||
("NE", "Nebraska (NE)"),
|
||||
("NV", "Nevada (NV)"),
|
||||
("NH", "New Hampshire (NH)"),
|
||||
("NJ", "New Jersey (NJ)"),
|
||||
("NM", "New Mexico (NM)"),
|
||||
("NY", "New York (NY)"),
|
||||
("NC", "North Carolina (NC)"),
|
||||
("ND", "North Dakota (ND)"),
|
||||
("MP", "Northern Mariana Islands (MP)"),
|
||||
("OH", "Ohio (OH)"),
|
||||
("OK", "Oklahoma (OK)"),
|
||||
("OR", "Oregon (OR)"),
|
||||
("PA", "Pennsylvania (PA)"),
|
||||
("PR", "Puerto Rico (PR)"),
|
||||
("RI", "Rhode Island (RI)"),
|
||||
("SC", "South Carolina (SC)"),
|
||||
("SD", "South Dakota (SD)"),
|
||||
("TN", "Tennessee (TN)"),
|
||||
("TX", "Texas (TX)"),
|
||||
("UM", "United States Minor Outlying Islands (UM)"),
|
||||
("UT", "Utah (UT)"),
|
||||
("VT", "Vermont (VT)"),
|
||||
("VI", "Virgin Islands (VI)"),
|
||||
("VA", "Virginia (VA)"),
|
||||
("WA", "Washington (WA)"),
|
||||
("WV", "West Virginia (WV)"),
|
||||
("WI", "Wisconsin (WI)"),
|
||||
("WY", "Wyoming (WY)"),
|
||||
("AA", "Armed Forces Americas (AA)"),
|
||||
("AE", "Armed Forces Africa, Canada, Europe, Middle East (AE)"),
|
||||
("AP", "Armed Forces Pacific (AP)"),
|
||||
],
|
||||
help_text="State, territory, or military post",
|
||||
max_length=2,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"zipcode",
|
||||
models.CharField(blank=True, db_index=True, help_text="Zip code", max_length=10, null=True),
|
||||
),
|
||||
(
|
||||
"urbanization",
|
||||
models.CharField(blank=True, help_text="Urbanization (required for Puerto Rico only)", null=True),
|
||||
),
|
||||
(
|
||||
"about_your_organization",
|
||||
models.TextField(blank=True, help_text="Information about your organization", null=True),
|
||||
),
|
||||
("purpose", models.TextField(blank=True, help_text="Purpose of your domain", null=True)),
|
||||
(
|
||||
"no_other_contacts_rationale",
|
||||
models.TextField(blank=True, help_text="Reason for listing no additional contacts", null=True),
|
||||
),
|
||||
("anything_else", models.TextField(blank=True, help_text="Anything else?", null=True)),
|
||||
(
|
||||
"is_policy_acknowledged",
|
||||
models.BooleanField(blank=True, help_text="Acknowledged .gov acceptable use policy", null=True),
|
||||
),
|
||||
("submission_date", models.DateField(blank=True, default=None, help_text="Date submitted", null=True)),
|
||||
("notes", models.TextField(blank=True, help_text="Notes about this request", null=True)),
|
||||
(
|
||||
"alternative_domains",
|
||||
models.ManyToManyField(blank=True, related_name="alternatives+", to="registrar.website"),
|
||||
),
|
||||
(
|
||||
"approved_domain",
|
||||
models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The approved domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_request",
|
||||
to="registrar.domain",
|
||||
),
|
||||
),
|
||||
(
|
||||
"authorizing_official",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="authorizing_official",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
(
|
||||
"creator",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_requests_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"current_websites",
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name="current+", to="registrar.website", verbose_name="websites"
|
||||
),
|
||||
),
|
||||
(
|
||||
"investigator",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="domain_requests_investigating",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"other_contacts",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
(
|
||||
"requested_domain",
|
||||
models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="The requested domain",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_request",
|
||||
to="registrar.draftdomain",
|
||||
),
|
||||
),
|
||||
(
|
||||
"submitter",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="domaininformation",
|
||||
name="domain_application",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="contacts",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="submitted_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="DomainApplication",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="domain_request",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="Associated domain request",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="DomainRequest_info",
|
||||
to="registrar.domainrequest",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-12 16:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0073_alter_domainapplication_approved_domain_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="DomainApplication",
|
||||
new_name="DomainRequest",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="domaininformation",
|
||||
old_name="domain_application",
|
||||
new_name="domain_request",
|
||||
),
|
||||
]
|
|
@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any:
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0073_domainrequest_and_more"),
|
||||
("registrar", "0074_rename_domainapplication_domainrequest_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -9,6 +9,7 @@ from django.db import models
|
|||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.utility.errors import FSMApplicationError, FSMErrorCodes
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
|
@ -645,6 +646,14 @@ class DomainRequest(TimeStampedModel):
|
|||
except EmailSendingError:
|
||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||
|
||||
def investigator_exists_and_is_staff(self):
|
||||
"""Checks if the current investigator is in a valid state for a state transition"""
|
||||
is_valid = True
|
||||
# Check if an investigator is assigned. No approval is possible without one.
|
||||
if self.investigator is None or not self.investigator.is_staff:
|
||||
is_valid = False
|
||||
return is_valid
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[
|
||||
|
@ -656,7 +665,7 @@ class DomainRequest(TimeStampedModel):
|
|||
target=DomainRequestStatus.SUBMITTED,
|
||||
)
|
||||
def submit(self):
|
||||
"""Submit a domain request that is started.
|
||||
"""Submit an domain request that is started.
|
||||
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
|
@ -664,10 +673,7 @@ class DomainRequest(TimeStampedModel):
|
|||
# can raise more informative exceptions
|
||||
|
||||
# requested_domain could be None here
|
||||
if not hasattr(self, "requested_domain"):
|
||||
raise ValueError("Requested domain is missing.")
|
||||
|
||||
if self.requested_domain is None:
|
||||
if not hasattr(self, "requested_domain") or self.requested_domain is None:
|
||||
raise ValueError("Requested domain is missing.")
|
||||
|
||||
DraftDomain = apps.get_model("registrar.DraftDomain")
|
||||
|
@ -704,10 +710,10 @@ class DomainRequest(TimeStampedModel):
|
|||
DomainRequestStatus.INELIGIBLE,
|
||||
],
|
||||
target=DomainRequestStatus.IN_REVIEW,
|
||||
conditions=[domain_is_not_active],
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def in_review(self):
|
||||
"""Investigate a domain request that has been submitted.
|
||||
"""Investigate an domain request that has been submitted.
|
||||
|
||||
This action is logged.
|
||||
|
||||
|
@ -736,10 +742,10 @@ class DomainRequest(TimeStampedModel):
|
|||
DomainRequestStatus.INELIGIBLE,
|
||||
],
|
||||
target=DomainRequestStatus.ACTION_NEEDED,
|
||||
conditions=[domain_is_not_active],
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def action_needed(self):
|
||||
"""Send back a domain request that is under investigation or rejected.
|
||||
"""Send back an domain request that is under investigation or rejected.
|
||||
|
||||
This action is logged.
|
||||
|
||||
|
@ -768,9 +774,10 @@ class DomainRequest(TimeStampedModel):
|
|||
DomainRequestStatus.REJECTED,
|
||||
],
|
||||
target=DomainRequestStatus.APPROVED,
|
||||
conditions=[investigator_exists_and_is_staff],
|
||||
)
|
||||
def approve(self, send_email=True):
|
||||
"""Approve a domain request that has been submitted.
|
||||
"""Approve an domain request that has been submitted.
|
||||
|
||||
This action cleans up the rejection status if moving away from rejected.
|
||||
|
||||
|
@ -781,8 +788,12 @@ class DomainRequest(TimeStampedModel):
|
|||
|
||||
# create the domain
|
||||
Domain = apps.get_model("registrar.Domain")
|
||||
|
||||
# == Check that the domain_request is valid == #
|
||||
if Domain.objects.filter(name=self.requested_domain.name).exists():
|
||||
raise ValueError("Cannot approve. Requested domain is already in use.")
|
||||
raise FSMApplicationError(code=FSMErrorCodes.APPROVE_DOMAIN_IN_USE)
|
||||
|
||||
# == Create the domain and related components == #
|
||||
created_domain = Domain.objects.create(name=self.requested_domain.name)
|
||||
self.approved_domain = created_domain
|
||||
|
||||
|
@ -799,6 +810,7 @@ class DomainRequest(TimeStampedModel):
|
|||
if self.status == self.DomainRequestStatus.REJECTED:
|
||||
self.rejection_reason = None
|
||||
|
||||
# == Send out an email == #
|
||||
self._send_status_update_email(
|
||||
"domain request approved",
|
||||
"emails/status_change_approved.txt",
|
||||
|
@ -812,7 +824,7 @@ class DomainRequest(TimeStampedModel):
|
|||
target=DomainRequestStatus.WITHDRAWN,
|
||||
)
|
||||
def withdraw(self):
|
||||
"""Withdraw a domain request that has been submitted."""
|
||||
"""Withdraw an domain request that has been submitted."""
|
||||
|
||||
self._send_status_update_email(
|
||||
"withdraw",
|
||||
|
@ -824,10 +836,10 @@ class DomainRequest(TimeStampedModel):
|
|||
field="status",
|
||||
source=[DomainRequestStatus.IN_REVIEW, DomainRequestStatus.ACTION_NEEDED, DomainRequestStatus.APPROVED],
|
||||
target=DomainRequestStatus.REJECTED,
|
||||
conditions=[domain_is_not_active],
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def reject(self):
|
||||
"""Reject a domain request that has been submitted.
|
||||
"""Reject an domain request that has been submitted.
|
||||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade), and send an email notification."""
|
||||
|
@ -850,7 +862,7 @@ class DomainRequest(TimeStampedModel):
|
|||
DomainRequestStatus.REJECTED,
|
||||
],
|
||||
target=DomainRequestStatus.INELIGIBLE,
|
||||
conditions=[domain_is_not_active],
|
||||
conditions=[domain_is_not_active, investigator_exists_and_is_staff],
|
||||
)
|
||||
def reject_with_prejudice(self):
|
||||
"""The applicant is a bad actor, reject with prejudice.
|
||||
|
|
|
@ -529,6 +529,7 @@ def completed_domain_request(
|
|||
user=False,
|
||||
submitter=False,
|
||||
name="city.gov",
|
||||
investigator=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -558,6 +559,13 @@ def completed_domain_request(
|
|||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
if not investigator:
|
||||
investigator, _ = User.objects.get_or_create(
|
||||
username="incrediblyfakeinvestigator",
|
||||
first_name="Joe",
|
||||
last_name="Bob",
|
||||
is_staff=True,
|
||||
)
|
||||
domain_request_kwargs = dict(
|
||||
organization_type="federal",
|
||||
federal_type="executive",
|
||||
|
@ -573,6 +581,7 @@ def completed_domain_request(
|
|||
submitter=submitter,
|
||||
creator=user,
|
||||
status=status,
|
||||
investigator=investigator,
|
||||
)
|
||||
if has_about_your_organization:
|
||||
domain_request_kwargs["about_your_organization"] = "e-Government"
|
||||
|
@ -591,6 +600,13 @@ def completed_domain_request(
|
|||
return domain_request
|
||||
|
||||
|
||||
def set_domain_request_investigators(domain_request_list: list[DomainRequest], investigator_user: User):
|
||||
"""Helper method that sets the investigator field of all provided domain requests"""
|
||||
for request in domain_request_list:
|
||||
request.investigator = investigator_user
|
||||
request.save()
|
||||
|
||||
|
||||
def multiple_unalphabetical_domain_objects(
|
||||
domain_type=AuditedAdminMockData.DOMAIN_REQUEST,
|
||||
):
|
||||
|
|
|
@ -17,7 +17,7 @@ from registrar.models import (
|
|||
import boto3_mocking
|
||||
from registrar.models.transition_domain import TransitionDomain
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore
|
||||
from .common import MockSESClient, less_console_noise, completed_domain_request
|
||||
from .common import MockSESClient, less_console_noise, completed_domain_request, set_domain_request_investigators
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
|
||||
|
@ -52,6 +52,18 @@ class TestDomainRequest(TestCase):
|
|||
status=DomainRequest.DomainRequestStatus.INELIGIBLE, name="ineligible.gov"
|
||||
)
|
||||
|
||||
# Store all domain request statuses in a variable for ease of use
|
||||
self.all_domain_requests = [
|
||||
self.started_domain_request,
|
||||
self.submitted_domain_request,
|
||||
self.in_review_domain_request,
|
||||
self.action_needed_domain_request,
|
||||
self.approved_domain_request,
|
||||
self.withdrawn_domain_request,
|
||||
self.rejected_domain_request,
|
||||
self.ineligible_domain_request,
|
||||
]
|
||||
|
||||
self.mock_client = MockSESClient()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -219,6 +231,65 @@ class TestDomainRequest(TestCase):
|
|||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.check_email_sent(domain_request, msg, "reject_with_prejudice", 0)
|
||||
|
||||
def assert_fsm_transition_raises_error(self, test_cases, method_to_run):
|
||||
"""Given a list of test cases, check if each transition throws the intended error"""
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client), less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
# Retrieve the method by name from the domain_request object and call it
|
||||
method = getattr(domain_request, method_to_run)
|
||||
# Call the method
|
||||
method()
|
||||
|
||||
def assert_fsm_transition_does_not_raise_error(self, test_cases, method_to_run):
|
||||
"""Given a list of test cases, ensure that none of them throw transition errors"""
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client), less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
# Retrieve the method by name from the DomainRequest object and call it
|
||||
method = getattr(domain_request, method_to_run)
|
||||
# Call the method
|
||||
method()
|
||||
except exception_type:
|
||||
self.fail(f"{exception_type} was raised, but it was not expected.")
|
||||
|
||||
def test_submit_transition_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator.
|
||||
For submit, this should be valid in all cases.
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.started_domain_request, TransitionNotAllowed),
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.withdrawn_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "submit")
|
||||
|
||||
def test_submit_transition_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition with an investigator user that is not staff.
|
||||
For submit, this should be valid in all cases.
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "submit")
|
||||
|
||||
def test_submit_transition_allowed(self):
|
||||
"""
|
||||
Test that calling submit from allowable statuses does raises TransitionNotAllowed.
|
||||
|
@ -230,14 +301,27 @@ class TestDomainRequest(TestCase):
|
|||
(self.withdrawn_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "submit")
|
||||
|
||||
def test_submit_transition_allowed_twice(self):
|
||||
"""
|
||||
Test that rotating between submit and in_review doesn't throw an error
|
||||
"""
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.submit()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
try:
|
||||
# Make a submission
|
||||
self.in_review_domain_request.submit()
|
||||
|
||||
# Rerun the old method to get back to the original state
|
||||
self.in_review_domain_request.in_review()
|
||||
|
||||
# Make another submission
|
||||
self.in_review_domain_request.submit()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
|
||||
self.assertEqual(self.in_review_domain_request.status, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
|
||||
def test_submit_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -250,12 +334,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.submit()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "submit")
|
||||
|
||||
def test_in_review_transition_allowed(self):
|
||||
"""
|
||||
|
@ -269,14 +348,43 @@ class TestDomainRequest(TestCase):
|
|||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.in_review()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "in_review")
|
||||
|
||||
def test_in_review_transition_not_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "in_review")
|
||||
|
||||
def test_in_review_transition_not_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition with an investigator that is not staff.
|
||||
This should throw an exception.
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "in_review")
|
||||
|
||||
def test_in_review_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -288,12 +396,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.withdrawn_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.in_review()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "in_review")
|
||||
|
||||
def test_action_needed_transition_allowed(self):
|
||||
"""
|
||||
|
@ -305,13 +408,43 @@ class TestDomainRequest(TestCase):
|
|||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.action_needed()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "action_needed")
|
||||
|
||||
def test_action_needed_transition_not_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "action_needed")
|
||||
|
||||
def test_action_needed_transition_not_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition with an investigator that is not staff
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "action_needed")
|
||||
|
||||
def test_action_needed_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -323,11 +456,8 @@ class TestDomainRequest(TestCase):
|
|||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.withdrawn_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.action_needed()
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "action_needed")
|
||||
|
||||
def test_approved_transition_allowed(self):
|
||||
"""
|
||||
|
@ -340,14 +470,40 @@ class TestDomainRequest(TestCase):
|
|||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.approve()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "approve")
|
||||
|
||||
def test_approved_transition_not_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "approve")
|
||||
|
||||
def test_approved_transition_not_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition with an investigator that is not staff
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "approve")
|
||||
|
||||
def test_approved_skips_sending_email(self):
|
||||
"""
|
||||
|
@ -372,13 +528,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.withdrawn_domain_request, TransitionNotAllowed),
|
||||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.approve()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "approve")
|
||||
|
||||
def test_withdraw_transition_allowed(self):
|
||||
"""
|
||||
|
@ -390,14 +540,42 @@ class TestDomainRequest(TestCase):
|
|||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.withdraw()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw")
|
||||
|
||||
def test_withdraw_transition_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator.
|
||||
For withdraw, this should be valid in all cases.
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.submitted_domain_request, TransitionNotAllowed),
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw")
|
||||
|
||||
def test_withdraw_transition_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition when investigator is not staff.
|
||||
For withdraw, this should be valid in all cases.
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.submitted_domain_request, TransitionNotAllowed),
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "withdraw")
|
||||
|
||||
def test_withdraw_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -411,12 +589,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.withdraw()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "withdraw")
|
||||
|
||||
def test_reject_transition_allowed(self):
|
||||
"""
|
||||
|
@ -428,14 +601,40 @@ class TestDomainRequest(TestCase):
|
|||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.reject()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "reject")
|
||||
|
||||
def test_reject_transition_not_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject")
|
||||
|
||||
def test_reject_transition_not_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition when investigator is not staff
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject")
|
||||
|
||||
def test_reject_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -449,12 +648,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.reject()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject")
|
||||
|
||||
def test_reject_with_prejudice_transition_allowed(self):
|
||||
"""
|
||||
|
@ -467,14 +661,42 @@ class TestDomainRequest(TestCase):
|
|||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
try:
|
||||
domain_request.reject_with_prejudice()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
self.assert_fsm_transition_does_not_raise_error(test_cases, "reject_with_prejudice")
|
||||
|
||||
def test_reject_with_prejudice_transition_not_allowed_with_no_investigator(self):
|
||||
"""
|
||||
Tests for attempting to transition without an investigator
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to none
|
||||
set_domain_request_investigators(self.all_domain_requests, None)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice")
|
||||
|
||||
def test_reject_with_prejudice_not_allowed_with_investigator_not_staff(self):
|
||||
"""
|
||||
Tests for attempting to transition when investigator is not staff
|
||||
"""
|
||||
|
||||
test_cases = [
|
||||
(self.in_review_domain_request, TransitionNotAllowed),
|
||||
(self.action_needed_domain_request, TransitionNotAllowed),
|
||||
(self.approved_domain_request, TransitionNotAllowed),
|
||||
(self.rejected_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
# Set all investigators to a user with no staff privs
|
||||
user, _ = User.objects.get_or_create(username="pancakesyrup", is_staff=False)
|
||||
set_domain_request_investigators(self.all_domain_requests, user)
|
||||
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice")
|
||||
|
||||
def test_reject_with_prejudice_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -487,12 +709,7 @@ class TestDomainRequest(TestCase):
|
|||
(self.ineligible_domain_request, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
for domain_request, exception_type in test_cases:
|
||||
with self.subTest(domain_request=domain_request, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
domain_request.reject_with_prejudice()
|
||||
self.assert_fsm_transition_raises_error(test_cases, "reject_with_prejudice")
|
||||
|
||||
def test_transition_not_allowed_approved_in_review_when_domain_is_active(self):
|
||||
"""Create a domain request with status approved, create a matching domain that
|
||||
|
@ -666,7 +883,10 @@ class TestPermissions(TestCase):
|
|||
def test_approval_creates_role(self):
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
domain_request = DomainRequest.objects.create(creator=user, requested_domain=draft_domain)
|
||||
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=user, requested_domain=draft_domain, investigator=investigator
|
||||
)
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
|
@ -697,10 +917,12 @@ class TestDomainInformation(TestCase):
|
|||
|
||||
@boto3_mocking.patching
|
||||
def test_approval_creates_info(self):
|
||||
self.maxDiff = None
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
domain_request = DomainRequest.objects.create(creator=user, requested_domain=draft_domain, notes="test notes")
|
||||
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=user, requested_domain=draft_domain, notes="test notes", investigator=investigator
|
||||
)
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
|
|
|
@ -324,7 +324,10 @@ class TestDomainCreation(MockEppLib):
|
|||
with less_console_noise():
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
domain_request = DomainRequest.objects.create(creator=user, requested_domain=draft_domain)
|
||||
investigator, _ = User.objects.get_or_create(username="frenchtoast", is_staff=True)
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=user, requested_domain=draft_domain, investigator=investigator
|
||||
)
|
||||
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
|
|
|
@ -71,6 +71,48 @@ class GenericError(Exception):
|
|||
return self._error_mapping.get(code)
|
||||
|
||||
|
||||
class FSMErrorCodes(IntEnum):
|
||||
"""Used when doing FSM transitions.
|
||||
Overview of generic error codes:
|
||||
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
|
||||
- 2 NO_INVESTIGATOR No investigator is assigned
|
||||
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
||||
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
|
||||
"""
|
||||
|
||||
APPROVE_DOMAIN_IN_USE = 1
|
||||
NO_INVESTIGATOR = 2
|
||||
INVESTIGATOR_NOT_STAFF = 3
|
||||
INVESTIGATOR_NOT_SUBMITTER = 4
|
||||
|
||||
|
||||
class FSMApplicationError(Exception):
|
||||
"""
|
||||
Used to raise exceptions when doing FSM Transitions.
|
||||
Uses `FSMErrorCodes` as an enum.
|
||||
"""
|
||||
|
||||
_error_mapping = {
|
||||
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
|
||||
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
if self.code in self._error_mapping:
|
||||
self.message = self._error_mapping.get(self.code)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
||||
|
||||
@classmethod
|
||||
def get_error_message(cls, code=None):
|
||||
return cls._error_mapping.get(code)
|
||||
|
||||
|
||||
class NameserverErrorCodes(IntEnum):
|
||||
"""Used in the NameserverError class for
|
||||
error mapping.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue