diff --git a/src/registrar/admin.py b/src/registrar/admin.py index e58251743..d179d5549 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1095,8 +1095,7 @@ class DomainRequestAdmin(ListHeaderAdmin): "Type of organization", { "fields": [ - "generic_org_type", - "is_election_board", + "organization_type", "federal_type", "federal_agency", "tribe_name", diff --git a/src/registrar/fixtures_domain_requests.py b/src/registrar/fixtures_domain_requests.py index 02efae5a9..ece1d0f7f 100644 --- a/src/registrar/fixtures_domain_requests.py +++ b/src/registrar/fixtures_domain_requests.py @@ -98,6 +98,8 @@ class DomainRequestFixture: def _set_non_foreign_key_fields(cls, da: DomainRequest, app: dict): """Helper method used by `load`.""" da.status = app["status"] if "status" in app else "started" + + # TODO for a future ticket: Allow for more than just "federal" here da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal" da.federal_agency = ( app["federal_agency"] @@ -235,9 +237,6 @@ class DomainFixture(DomainRequestFixture): ).last() logger.debug(f"Approving {domain_request} for {user}") - # 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: diff --git a/src/registrar/migrations/0081_domainrequest_organization_type.py b/src/registrar/migrations/0081_domainrequest_organization_type.py new file mode 100644 index 000000000..1d2185fbb --- /dev/null +++ b/src/registrar/migrations/0081_domainrequest_organization_type.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.10 on 2024-03-29 15:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0080_create_groups_v09"), + ] + + operations = [ + migrations.AddField( + model_name="domainrequest", + name="organization_type", + field=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"), + ("state_or_territory_election", "State or territory - Election"), + ("tribal_election", "Tribal - Election"), + ("county_election", "County - Election"), + ("city_election", "City - Election"), + ("special_district_election", "Special district - Election"), + ], + help_text="Type of organization - Election office", + max_length=255, + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 8fc697df5..b3d5b19ce 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -198,7 +198,7 @@ class Domain(TimeStampedModel, DomainHelper): is called in the validate function on the request/domain page throws- RegistryError or InvalidDomainError""" - + return True if not cls.string_could_be_domain(domain): logger.warning("Not a valid domain: %s" % str(domain)) # throw invalid domain error so that it can be caught in diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index f4581de93..2b08bf1d0 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -100,8 +100,8 @@ class DomainRequest(TimeStampedModel): class OrganizationChoices(models.TextChoices): """ Primary organization choices: - For use in django admin - Keys need to match OrganizationChoicesVerbose + For use in the request experience + Keys need to match OrganizationChoicesElectionOffice and OrganizationChoicesVerbose """ FEDERAL = "federal", "Federal" @@ -113,9 +113,38 @@ class DomainRequest(TimeStampedModel): SPECIAL_DISTRICT = "special_district", "Special district" SCHOOL_DISTRICT = "school_district", "School district" + class OrganizationChoicesElectionOffice(models.TextChoices): + """ + Primary organization choices for Django admin: + Keys need to match OrganizationChoices and OrganizationChoicesVerbose. + + The enums here come in two variants: + Regular (matches the choices from OrganizationChoices) + Election (Appends " - Election" to the string) + + When adding the election variant, you must append "_election" to the end of the string. + """ + # We can't inherit OrganizationChoices due to models.TextChoices being an enum. + # We can redefine these values instead. + FEDERAL = "federal", "Federal" + INTERSTATE = "interstate", "Interstate" + STATE_OR_TERRITORY = "state_or_territory", "State or territory" + TRIBAL = "tribal", "Tribal" + COUNTY = "county", "County" + CITY = "city", "City" + SPECIAL_DISTRICT = "special_district", "Special district" + SCHOOL_DISTRICT = "school_district", "School district" + + # Election variants + STATE_OR_TERRITORY_ELECTION = "state_or_territory_election", "State or territory - Election" + TRIBAL_ELECTION = "tribal_election", "Tribal - Election" + COUNTY_ELECTION = "county_election", "County - Election" + CITY_ELECTION = "city_election", "City - Election" + SPECIAL_DISTRICT_ELECTION = "special_district_election", "Special district - Election" + class OrganizationChoicesVerbose(models.TextChoices): """ - Secondary organization choices + Tertiary organization choices For use in the domain request form and on the templates Keys need to match OrganizationChoices """ @@ -406,6 +435,14 @@ class DomainRequest(TimeStampedModel): help_text="Type of organization", ) + organization_type = models.CharField( + max_length=255, + choices=OrganizationChoicesElectionOffice.choices, + null=True, + blank=True, + help_text="Type of organization - Election office", + ) + federally_recognized_tribe = models.BooleanField( null=True, help_text="Is the tribe federally recognized", @@ -449,6 +486,7 @@ class DomainRequest(TimeStampedModel): help_text="Organization name", db_index=True, ) + address_line1 = models.CharField( null=True, blank=True, @@ -525,6 +563,7 @@ class DomainRequest(TimeStampedModel): related_name="domain_request", on_delete=models.PROTECT, ) + alternative_domains = models.ManyToManyField( "registrar.Website", blank=True, diff --git a/src/registrar/signals.py b/src/registrar/signals.py index 4e7768ef4..e44e53ace 100644 --- a/src/registrar/signals.py +++ b/src/registrar/signals.py @@ -1,14 +1,121 @@ import logging -from django.db.models.signals import post_save +from django.db.models.signals import pre_save, post_save from django.dispatch import receiver -from .models import User, Contact +from .models import User, Contact, DomainRequest logger = logging.getLogger(__name__) +@receiver(pre_save, sender=DomainRequest) +def create_or_update_organization_type(sender, instance, **kwargs): + """The organization_type field on DomainRequest is consituted from the + generic_org_type and is_election_board fields. To keep the organization_type + field up to date, we need to update it before save based off of those field + values. + + If the instance is marked as an election board and the generic_org_type is not + one of the excluded types (FEDERAL, INTERSTATE, SCHOOL_DISTRICT), the + organization_type is set to a corresponding election variant. Otherwise, it directly + mirrors the generic_org_type value. + """ + if not isinstance(instance, DomainRequest): + # I don't see how this could possibly happen - but its still a good check to have. + # Lets force a fail condition rather than wait for one to happen, if this occurs. + raise ValueError("Type mismatch. The instance was not DomainRequest.") + + # == Init variables == # + # We can't grab the election variant if it is in federal, interstate, or school_district. + # The "election variant" is just the org name, with " - Election" appended to the end. + # For example, "School district - Election". + invalid_types = [ + DomainRequest.OrganizationChoices.FEDERAL, + DomainRequest.OrganizationChoices.INTERSTATE, + DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, + ] + + # TODO - maybe we need a check here for .filter then .get + is_new_instance = instance.id is None + + # A new record is added with organization_type not defined. + # This happens from the regular domain request flow. + if is_new_instance: + + # == Check for invalid conditions before proceeding == # + if instance.organization_type and instance.generic_org_type: + # Since organization type is linked with generic_org_type and election board, + # we have to update one or the other, not both. + raise ValueError("Cannot update organization_type and generic_org_type simultaneously.") + + # If no changes occurred, do nothing + if not instance.organization_type and not instance.generic_org_type: + return None + # == Program flow will halt here if there is no reason to update == # + + # == Update the linked values == # + # Find out which field needs updating + organization_type_needs_update = instance.organization_type is None + generic_org_type_needs_update = instance.generic_org_type is None + + # Update that field + if organization_type_needs_update: + _update_org_type_from_generic_org_and_election(instance, invalid_types) + elif generic_org_type_needs_update: + _update_generic_org_and_election_from_org_type(instance) + else: + + # Instance is already in the database, fetch its current state + current_instance = DomainRequest.objects.get(id=instance.id) + + # Check the new and old values + generic_org_type_changed = instance.generic_org_type != current_instance.generic_org_type + is_election_board_changed = instance.is_election_board != current_instance.is_election_board + organization_type_changed = instance.organization_type != current_instance.organization_type + + # == Check for invalid conditions before proceeding == # + if organization_type_changed and (generic_org_type_changed or is_election_board_changed): + # Since organization type is linked with generic_org_type and election board, + # we have to update one or the other, not both. + raise ValueError("Cannot update organization_type and generic_org_type simultaneously.") + + # If no changes occured, do nothing + if not organization_type_changed and (not generic_org_type_changed and not is_election_board_changed): + return None + # == Program flow will halt here if there is no reason to update == # + + # == Update the linked values == # + # Find out which field needs updating + organization_type_needs_update = generic_org_type_changed or is_election_board_changed + generic_org_type_needs_update = organization_type_changed + + # Update that field + if organization_type_needs_update: + _update_org_type_from_generic_org_and_election(instance, invalid_types) + elif generic_org_type_needs_update: + _update_generic_org_and_election_from_org_type(instance) + +def _update_org_type_from_generic_org_and_election(instance, invalid_types): + # TODO handle if generic_org_type is None + if instance.generic_org_type not in invalid_types and instance.is_election_board: + instance.organization_type = f"{instance.generic_org_type}_election" + else: + instance.organization_type = str(instance.generic_org_type) + + +def _update_generic_org_and_election_from_org_type(instance): + """Given a value for organization_type, update the + generic_org_type and is_election_board values.""" + # TODO find a better solution than this + current_org_type = str(instance.organization_type) + if "_election" in current_org_type: + instance.generic_org_type = current_org_type.split("_election")[0] + instance.is_election_board = True + else: + instance.organization_type = str(instance.generic_org_type) + instance.is_election_board = False + @receiver(post_save, sender=User) def handle_profile(sender, instance, **kwargs): """Method for when a User is saved.