diff --git a/docs/developer/README.md b/docs/developer/README.md index a06202d8d..f894955e5 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -250,7 +250,7 @@ type docker-compose run owasp ``` -# Images, stylesheets, and JavaScript +## Images, stylesheets, and JavaScript We use the U.S. Web Design System (USWDS) for styling our applications. @@ -262,7 +262,7 @@ Assets are stored in `registrar/assets` during development and served from `regi We utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package USWDS assets. -## Making and viewing style changes +### Making and viewing style changes When you run `docker-compose up` the `node` service in the container will begin to watch for changes in the `registrar/assets` folder, and will recompile once any changes are made. @@ -273,7 +273,11 @@ Within the `registrar/assets` folder, the `_theme` folder contains three files i You can also compile the **Sass** at any time using `npx gulp compile`. Similarly, you can copy over **other static assets** (images and javascript files), using `npx gulp copyAssets`. -## Upgrading USWDS and other JavaScript packages +### CSS class naming conventions + +We use the [CSS Block Element Modifier (BEM)](https://getbem.com/naming/) naming convention for our custom classes. This is in line with how USWDS [approaches](https://designsystem.digital.gov/whats-new/updates/2019/04/08/introducing-uswds-2-0/) their CSS class architecture and helps keep our code cohesive and readable. + +### Upgrading USWDS and other JavaScript packages Version numbers can be manually controlled in `package.json`. Edit that, if desired. diff --git a/src/djangooidc/exceptions.py b/src/djangooidc/exceptions.py index 226337f54..000c47649 100644 --- a/src/djangooidc/exceptions.py +++ b/src/djangooidc/exceptions.py @@ -33,8 +33,8 @@ class AuthenticationFailed(OIDCException): friendly_message = "This login attempt didn't work." -class NoStateDefined(OIDCException): - friendly_message = "The session state is None." +class StateMismatch(AuthenticationFailed): + friendly_message = "State mismatch. This login attempt didn't work." class InternalError(OIDCException): diff --git a/src/djangooidc/oidc.py b/src/djangooidc/oidc.py index bff766bb4..95ed322f5 100644 --- a/src/djangooidc/oidc.py +++ b/src/djangooidc/oidc.py @@ -182,10 +182,20 @@ class Client(oic.Client): if authn_response["state"] != session.get("state", None): # this most likely means the user's Django session vanished - logger.error("Received state not the same as expected for %s" % state) if session.get("state", None) is None: - raise o_e.NoStateDefined() - raise o_e.AuthenticationFailed(locator=state) + logger.error( + f"The OP state {state} does not match the session state. " + f"The session state is None. " + f"authn_response['state'] = {authn_response['state']} " + f"session.get('state', None) = {session.get('state', None)}" + ) + else: + logger.error( + f"The OP state {state} does not match the session state. " + f"authn_response['state'] = {authn_response['state']} " + f"session.get('state', None) = {session.get('state', None)}" + ) + raise o_e.StateMismatch() if self.behaviour.get("response_type") == "code": # need an access token to get user info (and to log the user out later) diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 0f734b80d..f10afcbaf 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -4,7 +4,7 @@ from django.http import HttpResponse from django.test import Client, TestCase, RequestFactory from django.urls import reverse -from djangooidc.exceptions import NoStateDefined, InternalError +from djangooidc.exceptions import StateMismatch, InternalError from ..views import login_callback from .common import less_console_noise @@ -129,21 +129,35 @@ class ViewsTest(TestCase): self.assertContains(response, "Hi") def test_login_callback_with_no_session_state(self, mock_client): - """If the local session is None (ie the server restarted while user was logged out), + """If the local session does not match the OP session, we do not throw an exception. Rather, we attempt to login again.""" with less_console_noise(): - # MOCK - # mock the acr_value to some string - # mock the callback function to raise the NoStateDefined Exception + # MOCK get_default_acr_value and the callback to raise StateMismatch + # error when called mock_client.get_default_acr_value.side_effect = self.create_acr - mock_client.callback.side_effect = NoStateDefined() - # TEST - # test the login callback + mock_client.callback.side_effect = StateMismatch() + # TEST receiving a response from login.gov response = self.client.get(reverse("openid_login_callback")) - # ASSERTIONS - # assert that the user is redirected to the start of the login process + # ASSERT self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/") + # Check that the redirect_attempted flag is set in the session + self.assertTrue(self.client.session.get("redirect_attempted", False)) + + def test_login_callback_with_no_session_state_attempt_again_only_once(self, mock_client): + """We only attempt to relogin once. After that, it's the error page for you.""" + with less_console_noise(): + # MOCK get_default_acr_value, redirect_attempted to True and the callback + # to raise StateMismatch error when called + mock_client.get_default_acr_value.side_effect = self.create_acr + mock_client.callback.side_effect = StateMismatch() + session = self.client.session + session["redirect_attempted"] = True + session.save() + # TEST receiving a response from login.gov + response = self.client.get(reverse("openid_login_callback")) + # ASSERT + self.assertEqual(response.status_code, 401) def test_login_callback_reads_next(self, mock_client): """If the next value is set in the session, test that login_callback returns diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 8e112769b..ab81ccff1 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -101,16 +101,25 @@ def login_callback(request): if user: login(request, user) logger.info("Successfully logged in user %s" % user) - # Double login bug (1507)? + # Clear the flag if the exception is not caught + request.session.pop("redirect_attempted", None) return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() - except o_e.NoStateDefined as nsd_err: - # In the event that a user is in the middle of a login when the app is restarted, - # their session state will no longer be available, so redirect the user to the - # beginning of login process without raising an error to the user. - logger.warning(f"No State Defined: {nsd_err}") - return redirect(request.session.get("next", "/")) + except o_e.StateMismatch as nsd_err: + # Check if the redirect has already been attempted + if not request.session.get("redirect_attempted", False): + # Set the flag to indicate that the redirect has been attempted + request.session["redirect_attempted"] = True + + # In the event of a state mismatch between OP and session, redirect the user to the + # beginning of login process without raising an error to the user. Attempt once. + logger.warning(f"No State Defined: {nsd_err}") + return redirect(request.session.get("next", "/")) + else: + # Clear the flag if the exception is not caught + request.session.pop("redirect_attempted", None) + return error_page(request, nsd_err) except Exception as err: return error_page(request, err) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index f18fdd26a..6d1f588fb 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -16,7 +16,7 @@ from django.urls import reverse 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.errors import FSMApplicationError, FSMErrorCodes +from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from registrar.views.utility.mixins import OrderableFieldsMixin from django.contrib.admin.views.main import ORDER_VAR from registrar.widgets import NoAutocompleteFilteredSelectMultiple @@ -133,6 +133,7 @@ class DomainRequestAdminForm(forms.ModelForm): cleaned_data = super().clean() status = cleaned_data.get("status") investigator = cleaned_data.get("investigator") + rejection_reason = cleaned_data.get("rejection_reason") # Get the old status initial_status = self.initial.get("status", None) @@ -153,8 +154,32 @@ class DomainRequestAdminForm(forms.ModelForm): # Will call "add_error" if any issues are found. self._check_for_valid_investigator(investigator) + # If the status is rejected, a rejection reason must exist + if status == DomainRequest.DomainRequestStatus.REJECTED: + self._check_for_valid_rejection_reason(rejection_reason) + return cleaned_data + def _check_for_valid_rejection_reason(self, rejection_reason) -> bool: + """ + Checks if the rejection_reason field is not none. + Adds form errors on failure. + """ + is_valid = False + + # Check if a rejection reason exists. Rejection is not possible without one. + error_message = None + if rejection_reason is None or rejection_reason == "": + # Lets grab the error message from a common location + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_REJECTION_REASON) + else: + is_valid = True + + if error_message is not None: + self.add_error("rejection_reason", error_message) + + return is_valid + def _check_for_valid_investigator(self, investigator) -> bool: """ Checks if the investigator field is not none, and is staff. @@ -167,9 +192,9 @@ class DomainRequestAdminForm(forms.ModelForm): 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) + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_INVESTIGATOR) elif not investigator.is_staff: - error_message = FSMApplicationError.get_error_message(FSMErrorCodes.INVESTIGATOR_NOT_STAFF) + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.INVESTIGATOR_NOT_STAFF) else: is_valid = True @@ -845,7 +870,7 @@ class DomainInformationAdmin(ListHeaderAdmin): # Columns list_display = [ "domain", - "organization_type", + "generic_org_type", "created_at", "submitter", ] @@ -856,7 +881,7 @@ class DomainInformationAdmin(ListHeaderAdmin): ] # Filters - list_filter = ["organization_type"] + list_filter = ["generic_org_type"] # Search search_fields = [ @@ -873,7 +898,7 @@ class DomainInformationAdmin(ListHeaderAdmin): "Type of organization", { "fields": [ - "organization_type", + "generic_org_type", "is_election_board", "federal_type", "federal_agency", @@ -1012,13 +1037,13 @@ class DomainRequestAdmin(ListHeaderAdmin): if self.value() == "0": return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None)) - change_form_template = "django/admin/domain_application_change_form.html" + change_form_template = "django/admin/domain_request_change_form.html" # Columns list_display = [ "requested_domain", "status", - "organization_type", + "generic_org_type", "federal_type", "federal_agency", "organization_name", @@ -1045,7 +1070,7 @@ class DomainRequestAdmin(ListHeaderAdmin): # Filters list_filter = ( "status", - "organization_type", + "generic_org_type", "federal_type", ElectionOfficeFilter, "rejection_reason", @@ -1083,7 +1108,7 @@ class DomainRequestAdmin(ListHeaderAdmin): "Type of organization", { "fields": [ - "organization_type", + "generic_org_type", "is_election_board", "federal_type", "federal_agency", @@ -1231,7 +1256,7 @@ class DomainRequestAdmin(ListHeaderAdmin): # 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." + error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.NO_REJECTION_REASON) else: # This is an fsm in model which will throw an error if the # transition condition is violated, so we roll back the @@ -1240,11 +1265,11 @@ class DomainRequestAdmin(ListHeaderAdmin): obj.status = original_obj.status # Try to perform the status change. - # Catch FSMApplicationError's and return the message, + # Catch FSMDomainRequestError's and return the message, # as these are typically user errors. try: selected_method() - except FSMApplicationError as err: + except FSMDomainRequestError as err: logger.warning(f"An error encountered when trying to change status: {err}") error_message = err.message @@ -1418,7 +1443,7 @@ class DomainAdmin(ListHeaderAdmin): # Columns list_display = [ "name", - "organization_type", + "generic_org_type", "federal_type", "federal_agency", "organization_name", @@ -1443,10 +1468,10 @@ class DomainAdmin(ListHeaderAdmin): # in autocomplete_fields for domain ordering = ["name"] - def organization_type(self, obj): - return obj.domain_info.get_organization_type_display() + def generic_org_type(self, obj): + return obj.domain_info.get_generic_org_type_display() - organization_type.admin_order_field = "domain_info__organization_type" # type: ignore + generic_org_type.admin_order_field = "domain_info__generic_org_type" # type: ignore def federal_agency(self, obj): return obj.domain_info.federal_agency if obj.domain_info else None @@ -1483,7 +1508,7 @@ class DomainAdmin(ListHeaderAdmin): state_territory.admin_order_field = "domain_info__state_territory" # type: ignore # Filters - list_filter = ["domain_info__organization_type", "domain_info__federal_type", ElectionOfficeFilter, "state"] + list_filter = ["domain_info__generic_org_type", "domain_info__federal_type", ElectionOfficeFilter, "state"] search_fields = ["name"] search_help_text = "Search by domain name." @@ -1822,6 +1847,13 @@ class VerifiedByStaffAdmin(ListHeaderAdmin): super().save_model(request, obj, form, change) +class FederalAgencyAdmin(ListHeaderAdmin): + list_display = ["agency"] + search_fields = ["agency"] + search_help_text = "Search by agency name." + ordering = ["agency"] + + admin.site.unregister(LogEntry) # Unregister the default registration admin.site.register(LogEntry, CustomLogEntryAdmin) admin.site.register(models.User, MyUserAdmin) @@ -1835,6 +1867,7 @@ admin.site.register(models.DomainInvitation, DomainInvitationAdmin) admin.site.register(models.DomainInformation, DomainInformationAdmin) admin.site.register(models.Domain, DomainAdmin) admin.site.register(models.DraftDomain, DraftDomainAdmin) +admin.site.register(models.FederalAgency, FederalAgencyAdmin) # Host and HostIP removed from django admin because changes in admin # do not propagate to registry and logic not applied admin.site.register(models.Host, MyHostAdmin) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index a4a9fca54..34e72929f 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -347,6 +347,32 @@ input.admin-confirm-button { color: $dhs-blue-70; } +.admin-icon-group { + position: relative; + display: flex; + align-items: center; + + .usa-button__icon { + position: absolute; + right: 0; + height: 100%; + } + + input { + // Allow for padding around the copy button + padding-right: 35px !important; + // Match the height of other inputs + min-height: 2.25rem !important; + } + +} + +td.font-size-sm { + button.usa-button__icon { + font-size: 16px; + } +} + details.dja-detail-table { background-color: var(--darkened-bg); display: inline-table; @@ -397,3 +423,7 @@ td.font-size-sm { .no-outline-on-click:focus { outline: none !important; } + +.errors span.select2-selection { + border: 1px solid var(--error-fg) !important; +} \ No newline at end of file diff --git a/src/registrar/fixtures_domain_requests.py b/src/registrar/fixtures_domain_requests.py index a37e29d6b..02efae5a9 100644 --- a/src/registrar/fixtures_domain_requests.py +++ b/src/registrar/fixtures_domain_requests.py @@ -30,7 +30,7 @@ class DomainRequestFixture: # { # "status": "started", # "organization_name": "Example - Just started", - # "organization_type": "federal", + # "generic_org_type": "federal", # "federal_agency": None, # "federal_type": None, # "address_line1": None, @@ -98,7 +98,7 @@ 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" - da.organization_type = app["organization_type"] if "organization_type" in app else "federal" + da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal" da.federal_agency = ( app["federal_agency"] if "federal_agency" in app diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 22d76c768..0bfd9b667 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -262,8 +262,8 @@ class AuthorizingOfficialContactForm(ContactForm): return super().save() # Determine if the domain is federal or tribal - is_federal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.FEDERAL - is_tribal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.TRIBAL + is_federal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + is_tribal = self.domainInfo.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL # Get the Contact object from the db for the Authorizing Official db_ao = Contact.objects.get(id=self.instance.id) @@ -363,8 +363,8 @@ class DomainOrgNameAddressForm(forms.ModelForm): self.fields["state_territory"].widget.attrs.pop("maxlength", None) self.fields["zipcode"].widget.attrs.pop("maxlength", None) - self.is_federal = self.instance.organization_type == DomainRequest.OrganizationChoices.FEDERAL - self.is_tribal = self.instance.organization_type == DomainRequest.OrganizationChoices.TRIBAL + self.is_federal = self.instance.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL + self.is_tribal = self.instance.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL field_to_disable = None if self.is_federal: @@ -384,9 +384,9 @@ class DomainOrgNameAddressForm(forms.ModelForm): # If they get past this point, we forbid it this way. # This could be malicious, so lets reserve information for the backend only. if self.is_federal and not self._field_unchanged("federal_agency"): - raise ValueError("federal_agency cannot be modified when the organization_type is federal") + raise ValueError("federal_agency cannot be modified when the generic_org_type is federal") elif self.is_tribal and not self._field_unchanged("organization_name"): - raise ValueError("organization_name cannot be modified when the organization_type is tribal") + raise ValueError("organization_name cannot be modified when the generic_org_type is tribal") else: super().save() diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index ef47143ea..1efc028f6 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -169,7 +169,7 @@ class RegistrarFormSet(forms.BaseFormSet): class OrganizationTypeForm(RegistrarForm): - organization_type = forms.ChoiceField( + generic_org_type = forms.ChoiceField( # use the long names in the domain request form choices=DomainRequest.OrganizationChoicesVerbose.choices, widget=forms.RadioSelect, diff --git a/src/registrar/management/commands/transfer_transition_domains_to_domains.py b/src/registrar/management/commands/transfer_transition_domains_to_domains.py index 7f09d8de7..4ea74e335 100644 --- a/src/registrar/management/commands/transfer_transition_domains_to_domains.py +++ b/src/registrar/management/commands/transfer_transition_domains_to_domains.py @@ -332,7 +332,7 @@ class Command(BaseCommand): updated = False fields_to_update = [ - "organization_type", + "generic_org_type", "federal_type", "federal_agency", "organization_name", @@ -400,7 +400,7 @@ class Command(BaseCommand): if debug_on: logger.info(f"Contact created: {contact}") - org_type_current = transition_domain.organization_type + org_type_current = transition_domain.generic_org_type match org_type_current: case "Federal": org_type = ("federal", "Federal") @@ -431,7 +431,7 @@ class Command(BaseCommand): } if valid_org_type: - new_domain_info_data["organization_type"] = org_type[0] + new_domain_info_data["generic_org_type"] = org_type[0] elif debug_on: logger.debug(f"No org type found on {domain.name}") diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index 5c3573fb1..eb41c4be8 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -200,7 +200,7 @@ class LoadExtraTransitionDomain: updated_fields = [ "organization_name", - "organization_type", + "generic_org_type", "federal_type", "federal_agency", "first_name", @@ -412,7 +412,7 @@ class LoadExtraTransitionDomain: return transition_domain def parse_domain_type_data(self, domain_name, transition_domain: TransitionDomain) -> TransitionDomain: - """Grabs organization_type and federal_type from the parsed files + """Grabs generic_org_type and federal_type from the parsed files and associates it with a transition_domain object, then returns that object.""" if not isinstance(transition_domain, TransitionDomain): raise ValueError("Not a valid object, must be TransitionDomain") @@ -439,7 +439,7 @@ class LoadExtraTransitionDomain: raise ValueError("Found invalid data on DOMAIN_ADHOC") # Then, just grab the organization type. - new_organization_type = domain_type[0].strip() + new_generic_org_type = domain_type[0].strip() # Check if this domain_type is active or not. # If not, we don't want to add this. @@ -455,8 +455,8 @@ class LoadExtraTransitionDomain: # Are we updating data that already exists, # or are we adding new data in its place? - organization_type_exists = ( - transition_domain.organization_type is not None and transition_domain.organization_type.strip() != "" + generic_org_type_exists = ( + transition_domain.generic_org_type is not None and transition_domain.generic_org_type.strip() != "" ) federal_type_exists = ( transition_domain.federal_type is not None and transition_domain.federal_type.strip() != "" @@ -467,20 +467,20 @@ class LoadExtraTransitionDomain: is_federal = domain_type_length == 2 if is_federal: new_federal_type = domain_type[1].strip() - transition_domain.organization_type = new_organization_type + transition_domain.generic_org_type = new_generic_org_type transition_domain.federal_type = new_federal_type else: - transition_domain.organization_type = new_organization_type + transition_domain.generic_org_type = new_generic_org_type transition_domain.federal_type = None # Logs if we either added to this property, # or modified it. self._add_or_change_message( EnumFilenames.DOMAIN_ADHOC, - "organization_type", - transition_domain.organization_type, + "generic_org_type", + transition_domain.generic_org_type, domain_name, - organization_type_exists, + generic_org_type_exists, ) self._add_or_change_message( diff --git a/src/registrar/migrations/0078_rename_organization_type_domaininformation_generic_org_type_and_more.py b/src/registrar/migrations/0078_rename_organization_type_domaininformation_generic_org_type_and_more.py new file mode 100644 index 000000000..fb9d65cce --- /dev/null +++ b/src/registrar/migrations/0078_rename_organization_type_domaininformation_generic_org_type_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.10 on 2024-03-20 21:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0077_alter_publiccontact_fax_alter_publiccontact_org_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="domaininformation", + old_name="organization_type", + new_name="generic_org_type", + ), + migrations.RenameField( + model_name="domainrequest", + old_name="organization_type", + new_name="generic_org_type", + ), + migrations.RenameField( + model_name="transitiondomain", + old_name="organization_type", + new_name="generic_org_type", + ), + ] diff --git a/src/registrar/migrations/0079_create_federal_agencies_v01.py b/src/registrar/migrations/0079_create_federal_agencies_v01.py new file mode 100644 index 000000000..2f42e3382 --- /dev/null +++ b/src/registrar/migrations/0079_create_federal_agencies_v01.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.10 on 2024-03-22 22:18 + +from django.db import migrations, models +from registrar.models import FederalAgency +from typing import Any + + +# For linting: RunPython expects a function reference. +def create_federal_agencies(apps, schema_editor) -> Any: + FederalAgency.create_federal_agencies(apps, schema_editor) + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0078_rename_organization_type_domaininformation_generic_org_type_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="FederalAgency", + 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)), + ("agency", models.CharField(blank=True, help_text="Federal agency", null=True)), + ], + options={ + "verbose_name": "Federal agency", + "verbose_name_plural": "Federal agencies", + }, + ), + migrations.RunPython( + create_federal_agencies, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/migrations/0080_create_groups_v09.py b/src/registrar/migrations/0080_create_groups_v09.py new file mode 100644 index 000000000..342404aa1 --- /dev/null +++ b/src/registrar/migrations/0080_create_groups_v09.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0079 (which populates federal agencies) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0079_create_federal_agencies_v01"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index d203421ac..d3bbb3ae5 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -4,6 +4,7 @@ from .domain_request import DomainRequest from .domain_information import DomainInformation from .domain import Domain from .draft_domain import DraftDomain +from .federal_agency import FederalAgency from .host_ip import HostIP from .host import Host from .domain_invitation import DomainInvitation @@ -22,6 +23,7 @@ __all__ = [ "Domain", "DraftDomain", "DomainInvitation", + "FederalAgency", "HostIP", "Host", "UserDomainRole", @@ -39,6 +41,7 @@ auditlog.register(Domain) auditlog.register(DraftDomain) auditlog.register(DomainInvitation) auditlog.register(DomainInformation) +auditlog.register(FederalAgency) auditlog.register(HostIP) auditlog.register(Host) auditlog.register(UserDomainRole) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index f8f4db9c6..b5755a3c9 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -49,7 +49,7 @@ class DomainInformation(TimeStampedModel): ) # ##### data fields from the initial form ##### - organization_type = models.CharField( + generic_org_type = models.CharField( max_length=255, choices=OrganizationChoices.choices, null=True, diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index e7378a880..f4581de93 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -9,7 +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 registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes from .utility.time_stamped_model import TimeStampedModel from ..utility.email import send_templated_email, EmailSendingError @@ -397,7 +397,7 @@ class DomainRequest(TimeStampedModel): ) # ##### data fields from the initial form ##### - organization_type = models.CharField( + generic_org_type = models.CharField( max_length=255, # use the short names in Django admin choices=OrganizationChoices.choices, @@ -791,7 +791,7 @@ class DomainRequest(TimeStampedModel): # == Check that the domain_request is valid == # if Domain.objects.filter(name=self.requested_domain.name).exists(): - raise FSMApplicationError(code=FSMErrorCodes.APPROVE_DOMAIN_IN_USE) + raise FSMDomainRequestError(code=FSMErrorCodes.APPROVE_DOMAIN_IN_USE) # == Create the domain and related components == # created_domain = Domain.objects.create(name=self.requested_domain.name) @@ -886,12 +886,12 @@ class DomainRequest(TimeStampedModel): def show_organization_federal(self) -> bool: """Show this step if the answer to the first question was "federal".""" - user_choice = self.organization_type + user_choice = self.generic_org_type return user_choice == DomainRequest.OrganizationChoices.FEDERAL def show_tribal_government(self) -> bool: """Show this step if the answer to the first question was "tribal".""" - user_choice = self.organization_type + user_choice = self.generic_org_type return user_choice == DomainRequest.OrganizationChoices.TRIBAL def show_organization_election(self) -> bool: @@ -900,7 +900,7 @@ class DomainRequest(TimeStampedModel): This shows for answers that aren't "Federal" or "Interstate". This also doesnt show if user selected "School District" as well (#524) """ - user_choice = self.organization_type + user_choice = self.generic_org_type excluded = [ DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.INTERSTATE, @@ -910,7 +910,7 @@ class DomainRequest(TimeStampedModel): def show_about_your_organization(self) -> bool: """Show this step if this is a special district or interstate.""" - user_choice = self.organization_type + user_choice = self.generic_org_type return user_choice in [ DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, DomainRequest.OrganizationChoices.INTERSTATE, @@ -927,12 +927,12 @@ class DomainRequest(TimeStampedModel): def is_federal(self) -> Union[bool, None]: """Is this domain request for a federal agency? - organization_type can be both null and blank, + generic_org_type can be both null and blank, """ - if not self.organization_type: - # organization_type is either blank or None, can't answer + if not self.generic_org_type: + # generic_org_type is either blank or None, can't answer return None - if self.organization_type == DomainRequest.OrganizationChoices.FEDERAL: + if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL: return True return False diff --git a/src/registrar/models/federal_agency.py b/src/registrar/models/federal_agency.py new file mode 100644 index 000000000..89b15ab56 --- /dev/null +++ b/src/registrar/models/federal_agency.py @@ -0,0 +1,223 @@ +from .utility.time_stamped_model import TimeStampedModel +from django.db import models +import logging + +logger = logging.getLogger(__name__) + + +class FederalAgency(TimeStampedModel): + class Meta: + verbose_name = "Federal agency" + verbose_name_plural = "Federal agencies" + + agency = models.CharField( + null=True, + blank=True, + help_text="Federal agency", + ) + + def __str__(self) -> str: + return f"{self.agency}" + + def create_federal_agencies(apps, schema_editor): + """This method gets run from a data migration to prepopulate data + regarding federal agencies.""" + + # Hard to pass self to these methods as the calls from migrations + # are only expecting apps and schema_editor, so we'll just define + # apps, schema_editor in the local scope instead + + AGENCIES = [ + "Administrative Conference of the United States", + "Advisory Council on Historic Preservation", + "American Battle Monuments Commission", + "AMTRAK", + "Appalachian Regional Commission", + ("Appraisal Subcommittee of the Federal Financial " "Institutions Examination Council"), + "Architect of the Capitol", + "Armed Forces Retirement Home", + "Barry Goldwater Scholarship and Excellence in Education Foundation", + "Central Intelligence Agency", + "Christopher Columbus Fellowship Foundation", + "Civil Rights Cold Case Records Review Board", + "Commission for the Preservation of America's Heritage Abroad", + "Commission of Fine Arts", + "Committee for Purchase From People Who Are Blind or Severely Disabled", + "Commodity Futures Trading Commission", + "Congressional Budget Office", + "Consumer Financial Protection Bureau", + "Consumer Product Safety Commission", + "Corporation for National and Community Service", + "Council of Inspectors General on Integrity and Efficiency", + "Court Services and Offender Supervision", + "Cyberspace Solarium Commission", + "DC Court Services and Offender Supervision Agency", + "DC Pre-trial Services", + "Defense Nuclear Facilities Safety Board", + "Delta Regional Authority", + "Denali Commission", + "Department of Agriculture", + "Department of Commerce", + "Department of Defense", + "Department of Education", + "Department of Energy", + "Department of Health and Human Services", + "Department of Homeland Security", + "Department of Housing and Urban Development", + "Department of Justice", + "Department of Labor", + "Department of State", + "Department of the Interior", + "Department of the Treasury", + "Department of Transportation", + "Department of Veterans Affairs", + "Director of National Intelligence", + "Dwight D. Eisenhower Memorial Commission", + "Election Assistance Commission", + "Environmental Protection Agency", + "Equal Employment Opportunity Commission", + "Executive Office of the President", + "Export-Import Bank of the United States", + "Farm Credit Administration", + "Farm Credit System Insurance Corporation", + "Federal Communications Commission", + "Federal Deposit Insurance Corporation", + "Federal Election Commission", + "Federal Energy Regulatory Commission", + "Federal Financial Institutions Examination Council", + "Federal Housing Finance Agency", + "Federal Judiciary", + "Federal Labor Relations Authority", + "Federal Maritime Commission", + "Federal Mediation and Conciliation Service", + "Federal Mine Safety and Health Review Commission", + "Federal Permitting Improvement Steering Council", + "Federal Reserve Board of Governors", + "Federal Trade Commission", + "General Services Administration", + "gov Administration", + "Government Accountability Office", + "Government Publishing Office", + "Gulf Coast Ecosystem Restoration Council", + "Harry S. Truman Scholarship Foundation", + "Institute of Museum and Library Services", + "Institute of Peace", + "Inter-American Foundation", + "International Boundary and Water Commission: United States and Mexico", + "International Boundary Commission: United States and Canada", + "International Joint Commission: United States and Canada", + "James Madison Memorial Fellowship Foundation", + "Japan-U.S. Friendship Commission", + "John F. Kennedy Center for the Performing Arts", + "Legal Services Corporation", + "Legislative Branch", + "Library of Congress", + "Marine Mammal Commission", + "Medicaid and CHIP Payment and Access Commission", + "Medicare Payment Advisory Commission", + "Merit Systems Protection Board", + "Millennium Challenge Corporation", + "Morris K. Udall and Stewart L. Udall Foundation", + "National Aeronautics and Space Administration", + "National Archives and Records Administration", + "National Capital Planning Commission", + "National Council on Disability", + "National Credit Union Administration", + "National Endowment for the Arts", + "National Endowment for the Humanities", + "National Foundation on the Arts and the Humanities", + "National Gallery of Art", + "National Indian Gaming Commission", + "National Labor Relations Board", + "National Mediation Board", + "National Science Foundation", + "National Security Commission on Artificial Intelligence", + "National Transportation Safety Board", + "Networking Information Technology Research and Development", + "Non-Federal Agency", + "Northern Border Regional Commission", + "Nuclear Regulatory Commission", + "Nuclear Safety Oversight Committee", + "Occupational Safety and Health Review Commission", + "Office of Compliance", + "Office of Congressional Workplace Rights", + "Office of Government Ethics", + "Office of Navajo and Hopi Indian Relocation", + "Office of Personnel Management", + "Open World Leadership Center", + "Overseas Private Investment Corporation", + "Peace Corps", + "Pension Benefit Guaranty Corporation", + "Postal Regulatory Commission", + "Presidio Trust", + "Privacy and Civil Liberties Oversight Board", + "Public Buildings Reform Board", + "Public Defender Service for the District of Columbia", + "Railroad Retirement Board", + "Securities and Exchange Commission", + "Selective Service System", + "Small Business Administration", + "Smithsonian Institution", + "Social Security Administration", + "Social Security Advisory Board", + "Southeast Crescent Regional Commission", + "Southwest Border Regional Commission", + "State Justice Institute", + "Stennis Center for Public Service", + "Surface Transportation Board", + "Tennessee Valley Authority", + "The Executive Office of the President", + "The Intelligence Community", + "The Legislative Branch", + "The Supreme Court", + "The United States World War One Centennial Commission", + "U.S. Access Board", + "U.S. Agency for Global Media", + "U.S. Agency for International Development", + "U.S. Capitol Police", + "U.S. Chemical Safety Board", + "U.S. China Economic and Security Review Commission", + "U.S. Commission for the Preservation of Americas Heritage Abroad", + "U.S. Commission of Fine Arts", + "U.S. Commission on Civil Rights", + "U.S. Commission on International Religious Freedom", + "U.S. Courts", + "U.S. Department of Agriculture", + "U.S. Interagency Council on Homelessness", + "U.S. International Trade Commission", + "U.S. Nuclear Waste Technical Review Board", + "U.S. Office of Special Counsel", + "U.S. Postal Service", + "U.S. Semiquincentennial Commission", + "U.S. Trade and Development Agency", + "U.S.-China Economic and Security Review Commission", + "Udall Foundation", + "United States AbilityOne", + "United States Access Board", + "United States African Development Foundation", + "United States Agency for Global Media", + "United States Arctic Research Commission", + "United States Global Change Research Program", + "United States Holocaust Memorial Museum", + "United States Institute of Peace", + "United States Interagency Council on Homelessness", + "United States International Development Finance Corporation", + "United States International Trade Commission", + "United States Postal Service", + "United States Senate", + "United States Trade and Development Agency", + "Utah Reclamation Mitigation and Conservation Commission", + "Vietnam Education Foundation", + "Western Hemisphere Drug Policy Commission", + "Woodrow Wilson International Center for Scholars", + "World War I Centennial Commission", + ] + + FederalAgency = apps.get_model("registrar", "FederalAgency") + logger.info("Creating federal agency table.") + + try: + agencies = [FederalAgency(agency=agency) for agency in AGENCIES] + FederalAgency.objects.bulk_create(agencies) + except Exception as e: + logger.error(f"Error creating federal agencies: {e}") diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py index 0c9c2ae66..eafbeda00 100644 --- a/src/registrar/models/transition_domain.py +++ b/src/registrar/models/transition_domain.py @@ -49,7 +49,7 @@ class TransitionDomain(TimeStampedModel): verbose_name="Processed", help_text="Indicates whether this TransitionDomain was already processed", ) - organization_type = models.CharField( + generic_org_type = models.CharField( max_length=255, null=True, blank=True, @@ -147,7 +147,7 @@ class TransitionDomain(TimeStampedModel): f"username: {self.username}, \n" f"status: {self.status}, \n" f"email sent: {self.email_sent}, \n" - f"organization type: {self.organization_type}, \n" + f"organization type: {self.generic_org_type}, \n" f"organization_name: {self.organization_name}, \n" f"federal_type: {self.federal_type}, \n" f"federal_agency: {self.federal_agency}, \n" diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index a84da798a..2aa2f642e 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -71,6 +71,11 @@ class UserGroup(Group): "model": "verifiedbystaff", "permissions": ["add_verifiedbystaff", "change_verifiedbystaff", "delete_verifiedbystaff"], }, + { + "app_label": "registrar", + "model": "federalagency", + "permissions": ["add_federalagency", "change_federalagency", "delete_federalagency"], + }, ] # Avoid error: You can't execute queries until the end diff --git a/src/registrar/templates/domain_authorizing_official.html b/src/registrar/templates/domain_authorizing_official.html index 2e2faa0d3..aa9808c2e 100644 --- a/src/registrar/templates/domain_authorizing_official.html +++ b/src/registrar/templates/domain_authorizing_official.html @@ -12,7 +12,7 @@

Your authorizing official is a person within your organization who can authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about who can serve as an authorizing official.

- {% if organization_type == "federal" or organization_type == "tribal" %} + {% if generic_org_type == "federal" or generic_org_type == "tribal" %}

The authorizing official for your organization can’t be updated here. To suggest an update, email help@get.gov. @@ -25,7 +25,7 @@

{% csrf_token %} - {% if organization_type == "federal" or organization_type == "tribal" %} + {% if generic_org_type == "federal" or generic_org_type == "tribal" %} {# If all fields are disabled, add SR content #}
{{ form.first_name.value }}
{{ form.last_name.value }}
@@ -41,7 +41,7 @@ {% input_with_errors form.email %} - {% if organization_type != "federal" and organization_type != "tribal" %} + {% if generic_org_type != "federal" and generic_org_type != "tribal" %} {% endif %}
diff --git a/src/registrar/templates/domain_org_name_address.html b/src/registrar/templates/domain_org_name_address.html index 180fdd2a6..1e6176aa0 100644 --- a/src/registrar/templates/domain_org_name_address.html +++ b/src/registrar/templates/domain_org_name_address.html @@ -11,12 +11,12 @@

The name of your organization will be publicly listed as the domain registrant.

- {% if domain.domain_info.organization_type == "federal" %} + {% if domain.domain_info.generic_org_type == "federal" %}

The federal agency for your organization can’t be updated here. To suggest an update, email help@get.gov.

- {% elif domain.domain_info.organization_type == "tribal" %} + {% elif domain.domain_info.generic_org_type == "tribal" %}

Your organization name can’t be updated here. To suggest an update, email help@get.gov. @@ -28,7 +28,7 @@

{% csrf_token %} - {% if domain.domain_info.organization_type == 'federal' %} + {% if domain.domain_info.generic_org_type == 'federal' %} {% input_with_errors form.federal_agency %} {% endif %} diff --git a/src/registrar/templates/domain_request_org_type.html b/src/registrar/templates/domain_request_org_type.html index eab61e0bc..0e01a5260 100644 --- a/src/registrar/templates/domain_request_org_type.html +++ b/src/registrar/templates/domain_request_org_type.html @@ -14,6 +14,6 @@ {% block form_fields %} {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %} - {% input_with_errors forms.0.organization_type %} + {% input_with_errors forms.0.generic_org_type %} {% endwith %} {% endblock %} diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 71aec8d0a..227fc0cea 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -24,8 +24,8 @@ {% if step == Step.ORGANIZATION_TYPE %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% if domain_request.organization_type is not None %} - {% with title=form_titles|get_item:step value=domain_request.get_organization_type_display|default:"Incomplete" %} + {% if domain_request.generic_org_type is not None %} + {% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete" %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% else %} diff --git a/src/registrar/templates/domain_request_status.html b/src/registrar/templates/domain_request_status.html index 55260db97..e6ca4cdb3 100644 --- a/src/registrar/templates/domain_request_status.html +++ b/src/registrar/templates/domain_request_status.html @@ -52,7 +52,7 @@

Summary of your domain request

{% with heading_level='h3' %} - {% with org_type=DomainRequest.get_organization_type_display %} + {% with org_type=DomainRequest.get_generic_org_type_display %} {% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %} {% endwith %} diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index e295d2f7e..7de159871 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -133,9 +133,11 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} + {% if invitation.status == invitation.DomainInvitationStatus.INVITED %} {% csrf_token %} + {% endif %} {% endfor %} diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index 8896bd85f..e426ae6ef 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -37,4 +37,6 @@ THANK YOU The .gov team Contact us: Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/emails/domain_request_withdrawn.txt b/src/registrar/templates/emails/domain_request_withdrawn.txt index 7860efe41..5ef429541 100644 --- a/src/registrar/templates/emails/domain_request_withdrawn.txt +++ b/src/registrar/templates/emails/domain_request_withdrawn.txt @@ -25,4 +25,6 @@ THANK YOU The .gov team Contact us: Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/emails/includes/domain_request_summary.txt b/src/registrar/templates/emails/includes/domain_request_summary.txt index 6c7b36523..10ec9ac2c 100644 --- a/src/registrar/templates/emails/includes/domain_request_summary.txt +++ b/src/registrar/templates/emails/includes/domain_request_summary.txt @@ -1,7 +1,7 @@ SUMMARY OF YOUR DOMAIN REQUEST Type of organization: -{{ domain_request.get_organization_type_display }} +{{ domain_request.get_generic_org_type_display }} {% if domain_request.show_organization_federal %} Federal government branch: {{ domain_request.get_federal_type_display }} diff --git a/src/registrar/templates/emails/status_change_approved.txt b/src/registrar/templates/emails/status_change_approved.txt index 7d97049c1..cab88d973 100644 --- a/src/registrar/templates/emails/status_change_approved.txt +++ b/src/registrar/templates/emails/status_change_approved.txt @@ -48,4 +48,6 @@ THANK YOU The .gov team Contact us: Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index 4573bc137..4febb4c64 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -78,4 +78,6 @@ THANK YOU The .gov team Contact us: Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/emails/submission_confirmation.txt b/src/registrar/templates/emails/submission_confirmation.txt index 1009efc02..c0ae3bb8f 100644 --- a/src/registrar/templates/emails/submission_confirmation.txt +++ b/src/registrar/templates/emails/submission_confirmation.txt @@ -37,4 +37,6 @@ THANK YOU The .gov team Contact us: Learn about .gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/emails/transition_domain_invitation.txt b/src/registrar/templates/emails/transition_domain_invitation.txt index 53fd7f9ee..bdce7d107 100644 --- a/src/registrar/templates/emails/transition_domain_invitation.txt +++ b/src/registrar/templates/emails/transition_domain_invitation.txt @@ -59,4 +59,6 @@ The .gov team .Gov blog Domain management Get.gov + +The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) {% endautoescape %} diff --git a/src/registrar/templates/includes/ao_example.html b/src/registrar/templates/includes/ao_example.html index 85df174aa..cddf0def7 100644 --- a/src/registrar/templates/includes/ao_example.html +++ b/src/registrar/templates/includes/ao_example.html @@ -24,28 +24,28 @@ {% endif %} -{% elif organization_type == 'city' %} +{% elif generic_org_type == 'city' %}

Cities

Domain requests from cities must be authorized by someone in a role of significant, executive responsibility within the city (mayor, council president, city manager, township/village supervisor, select board chairperson, chief, senior technology officer, or equivalent).

-{% elif organization_type == 'county' %} +{% elif generic_org_type == 'county' %}

Counties

Domain requests from counties must be authorized by the commission chair or someone in a role of significant, executive responsibility within the county (county judge, county mayor, parish/borough president, senior technology officer, or equivalent). Other county-level offices (county clerk, sheriff, county auditor, comptroller) may qualify, as well, in some instances.

-{% elif organization_type == 'interstate' %} +{% elif generic_org_type == 'interstate' %}

Interstate organizations

Domain requests from interstate organizations must be authorized by someone in a role of significant, executive responsibility within the organization (president, director, chair, senior technology officer, or equivalent) or one of the state’s governors or CIOs.

-{% elif organization_type == 'school_district' %} +{% elif generic_org_type == 'school_district' %}

School districts

Domain requests from school district governments must be authorized by someone in a role of significant, executive responsibility within the district (board chair, superintendent, senior technology officer, or equivalent).

-{% elif organization_type == 'special_district' %} +{% elif generic_org_type == 'special_district' %}

Special districts

Domain requests from special districts must be authorized by someone in a role of significant, executive responsibility within the district (CEO, chair, executive director, senior technology officer, or equivalent).

-{% elif organization_type == 'state_or_territory' %} +{% elif generic_org_type == 'state_or_territory' %}

U.S. states and territories

States and territories: executive branch

@@ -54,7 +54,7 @@

States and territories: judicial and legislative branches

Domain requests from state legislatures and courts must be authorized by an agency’s CIO or someone in a role of significant, executive responsibility within the agency.

-{% elif organization_type == 'tribal' %} +{% elif generic_org_type == 'tribal' %}

Tribal governments

Domain requests from federally-recognized tribal governments must be authorized by the tribal leader the Bureau of Indian Affairs recognizes.

Domain requests from state-recognized tribal governments must be authorized by the tribal leader the individual state recognizes.

diff --git a/src/registrar/templates/includes/domain_example.html b/src/registrar/templates/includes/domain_example.html index 74ab18b3b..05df00e7b 100644 --- a/src/registrar/templates/includes/domain_example.html +++ b/src/registrar/templates/includes/domain_example.html @@ -26,7 +26,7 @@ {% endif %} -{% elif organization_type == 'interstate' %} +{% elif generic_org_type == 'interstate' %}
  • EMScompact.gov
  • @@ -34,7 +34,7 @@
  • trpa.gov
-{% elif organization_type == 'state_or_territory' %} +{% elif generic_org_type == 'state_or_territory' %}

State .gov domains must include the two-letter state abbreviation or clearly spell out the state name.

Examples:

    @@ -44,7 +44,7 @@
  • Guam.gov
-{% elif organization_type == 'tribal' %} +{% elif generic_org_type == 'tribal' %}

Tribal domains may include the suffix -nsn, for native sovereign nation.

Examples:

    @@ -53,7 +53,7 @@
  • TulalipTribalCourt-nsn.gov
-{% elif organization_type == 'county' %} +{% elif generic_org_type == 'county' %}

Most county .gov domains must include the two-letter state abbreviation or the full state name. County names that aren’t shared by any other city, county, parish, town, borough, village or equivalent in the U.S. (at the time a domain is granted) don’t have to refer to their state in their domain name. Counties can include “county” in their domain to distinguish it from other places with similar names.

We use the Census Bureau’s National Places Gazetteer Files to determine if county names are unique.

@@ -65,7 +65,7 @@
  • MiamiDade.gov
  • -{% elif organization_type == 'city' %} +{% elif generic_org_type == 'city' %}

    Most city domains must include the two-letter state abbreviation or clearly spell out the state name. Using phrases like “City of” or “Town of” is optional.

    Cities that meet one of the criteria below don’t have to refer to their state in their domain name.

      @@ -81,7 +81,7 @@

    -{% elif organization_type == 'special_district' %} +{% elif generic_org_type == 'special_district' %}

    Domain names must represent your organization or institutional name, not solely the services you provide. It also needs to include your two-letter state abbreviation or clearly spell out the state name.

    Examples:

      @@ -90,7 +90,7 @@
    • UtahTrust.gov
    -{% elif organization_type == 'school_district' %} +{% elif generic_org_type == 'school_district' %}

    Domain names must represent your organization or institutional name.

    Examples:

      diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index ff7a6258c..dc42a5c1d 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -54,9 +54,9 @@ def contains_checkbox(html_list): @register.filter -def get_organization_long_name(organization_type): +def get_organization_long_name(generic_org_type): organization_choices_dict = dict(DomainRequest.OrganizationChoicesVerbose.choices) - long_form_type = organization_choices_dict[organization_type] + long_form_type = organization_choices_dict[generic_org_type] if long_form_type is None: logger.error("Organization type error, triggered by a template's custom filter") return "Error" diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index afc30a16f..78cfa1019 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -372,7 +372,7 @@ class AuditedAdminMockData: purpose (str - optional): Sets a domains purpose Returns: Dictionary: { - organization_type: str, + generic_org_type: str, federal_type: str, purpose: str, organization_name: str = "{} organization".format(item_name), @@ -390,7 +390,7 @@ class AuditedAdminMockData: """ # noqa creator = self.dummy_user(item_name, "creator") common_args = dict( - organization_type=org_type, + generic_org_type=org_type, federal_type=federal_type, purpose=purpose, organization_name="{} organization".format(item_name), @@ -572,74 +572,94 @@ class MockDb(TestCase): state=Domain.State.READY, first_ready=timezone.make_aware(datetime.combine(date.today() + timedelta(days=1), datetime.min.time())), ) + self.domain_11, _ = Domain.objects.get_or_create( + name="cdomain11.gov", state=Domain.State.READY, first_ready=timezone.now() + ) + self.domain_12, _ = Domain.objects.get_or_create( + name="zdomain12.gov", state=Domain.State.READY, first_ready=timezone.now() + ) self.domain_information_1, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_1, - organization_type="federal", + generic_org_type="federal", federal_agency="World War I Centennial Commission", federal_type="executive", is_election_board=True, ) self.domain_information_2, _ = DomainInformation.objects.get_or_create( - creator=self.user, domain=self.domain_2, organization_type="interstate", is_election_board=True + creator=self.user, domain=self.domain_2, generic_org_type="interstate", is_election_board=True ) self.domain_information_3, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_3, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=True, ) self.domain_information_4, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_4, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=True, ) self.domain_information_5, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_5, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) self.domain_information_6, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_6, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) self.domain_information_7, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_7, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) self.domain_information_8, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_8, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) self.domain_information_9, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_9, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) self.domain_information_10, _ = DomainInformation.objects.get_or_create( creator=self.user, domain=self.domain_10, - organization_type="federal", + generic_org_type="federal", federal_agency="Armed Forces Retirement Home", is_election_board=False, ) + self.domain_information_11, _ = DomainInformation.objects.get_or_create( + creator=self.user, + domain=self.domain_11, + generic_org_type="federal", + federal_agency="World War I Centennial Commission", + federal_type="executive", + is_election_board=True, + ) + self.domain_information_12, _ = DomainInformation.objects.get_or_create( + creator=self.user, + domain=self.domain_12, + generic_org_type="interstate", + is_election_board=False, + ) meoward_user = get_user_model().objects.create( username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com" @@ -665,6 +685,14 @@ class MockDb(TestCase): user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER ) + _, created = UserDomainRole.objects.get_or_create( + user=meoward_user, domain=self.domain_11, role=UserDomainRole.Roles.MANAGER + ) + + _, created = UserDomainRole.objects.get_or_create( + user=meoward_user, domain=self.domain_12, role=UserDomainRole.Roles.MANAGER + ) + with less_console_noise(): self.domain_request_1 = completed_domain_request( status=DomainRequest.DomainRequestStatus.STARTED, name="city1.gov" @@ -791,7 +819,7 @@ def completed_domain_request( is_staff=True, ) domain_request_kwargs = dict( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", is_policy_acknowledged=True, diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7e42cb0d5..2e4e6dfc6 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -285,9 +285,9 @@ class TestDomainAdmin(MockEppLib, WebTest): # There are 4 template references to Federal (4) plus four references in the table # for our actual domain_request - self.assertContains(response, "Federal", count=8) + self.assertContains(response, "Federal", count=36) # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) + self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist self.assertNotContains(response, "Federal: an agency of the U.S. government") @@ -556,6 +556,24 @@ class TestDomainRequestAdminForm(TestCase): expected_choices = [("started", "Started"), ("submitted", "Submitted")] self.assertEqual(form.fields["status"].widget.choices, expected_choices) + def test_form_no_rejection_reason(self): + with less_console_noise(): + # Create a form instance with the test domain request + form = DomainRequestAdminForm(instance=self.domain_request) + + form = DomainRequestAdminForm( + instance=self.domain_request, + data={ + "status": DomainRequest.DomainRequestStatus.REJECTED, + "rejection_reason": None, + }, + ) + self.assertFalse(form.is_valid()) + self.assertIn("rejection_reason", form.errors) + + rejection_reason = form.errors.get("rejection_reason") + self.assertEqual(rejection_reason, ["A rejection reason is required."]) + def test_form_choices_when_no_instance(self): with less_console_noise(): # Create a form instance without an instance @@ -691,9 +709,9 @@ class TestDomainRequestAdmin(MockEppLib): response = self.client.get("/admin/registrar/domainrequest/") # There are 4 template references to Federal (4) plus two references in the table # for our actual domain request - self.assertContains(response, "Federal", count=6) + self.assertContains(response, "Federal", count=34) # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) + self.assertContains(response, 'Federal', count=1) # Now let's make sure the long description does not exist self.assertNotContains(response, "Federal: an agency of the U.S. government") @@ -1454,7 +1472,7 @@ class TestDomainRequestAdmin(MockEppLib): "rejection_reason", "creator", "investigator", - "organization_type", + "generic_org_type", "federally_recognized_tribe", "state_recognized_tribe", "tribe_name", @@ -1664,7 +1682,7 @@ class TestDomainRequestAdmin(MockEppLib): readonly_fields = self.admin.get_list_filter(request) expected_fields = ( "status", - "organization_type", + "generic_org_type", "federal_type", DomainRequestAdmin.ElectionOfficeFilter, "rejection_reason", diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index 50861d97f..bf3b09d0d 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -39,6 +39,9 @@ class TestGroups(TestCase): "view_domaininvitation", "change_domainrequest", "change_draftdomain", + "add_federalagency", + "change_federalagency", + "delete_federalagency", "analyst_access_permission", "change_user", "delete_userdomainrole", diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 4d5315b80..c7fe5f94c 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -101,7 +101,7 @@ class TestDomainRequest(TestCase): domain_request = DomainRequest.objects.create( creator=user, investigator=user, - organization_type=DomainRequest.OrganizationChoices.FEDERAL, + generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, federal_type=DomainRequest.BranchChoices.EXECUTIVE, is_election_board=False, organization_name="Test", @@ -129,7 +129,7 @@ class TestDomainRequest(TestCase): domain, _ = Domain.objects.get_or_create(name="igorville.gov") information = DomainInformation.objects.create( creator=user, - organization_type=DomainInformation.OrganizationChoices.FEDERAL, + generic_org_type=DomainInformation.OrganizationChoices.FEDERAL, federal_type=DomainInformation.BranchChoices.EXECUTIVE, is_election_board=False, organization_name="Test", diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index b91f3bd18..5bd594a15 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -44,6 +44,7 @@ class CsvReportsTest(MockDb): fake_open = mock_open() expected_file_content = [ call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), + call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), @@ -65,6 +66,7 @@ class CsvReportsTest(MockDb): fake_open = mock_open() expected_file_content = [ call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), + call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), @@ -255,8 +257,10 @@ class ExportDataTest(MockDb, MockEppLib): "AO email,Security contact email,Status,Expiration date\n" "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" "adomain2.gov,Interstate,(blank),Dns needed\n" + "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n" - "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready" + "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready\n" + "zdomain12.govInterstateReady\n" ) # Normalize line endings and remove commas, # spaces and leading/trailing whitespace @@ -315,8 +319,10 @@ class ExportDataTest(MockDb, MockEppLib): "Security contact email,Status\n" "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" "adomain2.gov,Interstate,Dns needed\n" + "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" + "zdomain12.govInterstateReady\n" ) # Normalize line endings and remove commas, # spaces and leading/trailing whitespace @@ -341,9 +347,9 @@ class ExportDataTest(MockDb, MockEppLib): "State", "Security contact email", ] - sort_fields = ["domain__name", "federal_agency", "organization_type"] + sort_fields = ["domain__name", "federal_agency", "generic_org_type"] filter_condition = { - "organization_type__icontains": "federal", + "generic_org_type__icontains": "federal", "domain__state__in": [ Domain.State.READY, Domain.State.DNS_NEEDED, @@ -365,6 +371,7 @@ class ExportDataTest(MockDb, MockEppLib): "Domain name,Domain type,Agency,Organization name,City," "State,Security contact email\n" "adomain10.gov,Federal,Armed Forces Retirement Home\n" + "cdomain11.govFederal-ExecutiveWorldWarICentennialCommission\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" "ddomain3.gov,Federal,Armed Forces Retirement Home\n" ) @@ -455,6 +462,8 @@ class ExportDataTest(MockDb, MockEppLib): "State,Status,Expiration date\n" "cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n" "adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n" + "cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n" + "zdomain12.govInterstateReady\n" "zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" "sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" "xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n" @@ -515,9 +524,11 @@ class ExportDataTest(MockDb, MockEppLib): "Security contact email,Domain manager email 1,Domain manager email 2,Domain manager email 3\n" "adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n" "adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n" + "cdomain11.govReadyFederal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.com\n" "cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,," ", , , ,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n" "ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n" + "zdomain12.govReadyInterstatemeoward@rocks.com\n" ) # Normalize line endings and remove commas, # spaces and leading/trailing whitespace @@ -551,10 +562,12 @@ class ExportDataTest(MockDb, MockEppLib): "MANAGED DOMAINS COUNTS AT END DATE\n" "Total,Federal,Interstate,State or territory,Tribal,County,City," "Special district,School district,Election office\n" - "1,1,0,0,0,0,0,0,0,1\n" + "3,2,1,0,0,0,0,0,0,2\n" "\n" "Domain name,Domain type,Domain manager email 1,Domain manager email 2,Domain manager email 3\n" + "cdomain11.govFederal-Executivemeoward@rocks.com\n" "cdomain1.gov,Federal - Executive,meoward@rocks.com,info@example.com,big_lebowski@dude.co\n" + "zdomain12.govInterstatemeoward@rocks.com\n" ) # Normalize line endings and remove commas, @@ -674,12 +687,12 @@ class HelperFunctions(MockDb): } # Test with distinct managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition, True) - expected_content = [1, 1, 0, 0, 0, 0, 0, 0, 0, 1] + expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 2] self.assertEqual(managed_domains_sliced_at_end_date, expected_content) # Test without distinct managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition) - expected_content = [1, 3, 0, 0, 0, 0, 0, 0, 0, 1] + expected_content = [3, 4, 1, 0, 0, 0, 0, 0, 0, 2] self.assertEqual(managed_domains_sliced_at_end_date, expected_content) def test_get_sliced_requests(self): diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index 9008a4f94..9311b59f7 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -334,7 +334,7 @@ class TestOrganizationMigration(TestCase): domain_name="fakewebsite2.gov", status="on hold", email_sent=True, - organization_type="Federal", + generic_org_type="Federal", organization_name="Fanoodle", federal_type="Executive", federal_agency="Department of Commerce", @@ -399,7 +399,7 @@ class TestOrganizationMigration(TestCase): ).get() expected_domain_information = DomainInformation( creator=expected_creator, - organization_type="federal", + generic_org_type="federal", federal_agency="Department of Commerce", federal_type="executive", organization_name="Fanoodle", @@ -454,7 +454,7 @@ class TestOrganizationMigration(TestCase): ).get() expected_domain_information = DomainInformation( creator=expected_creator, - organization_type="federal", + generic_org_type="federal", federal_agency="Department of Commerce", federal_type="executive", organization_name="Fanoodle", @@ -794,7 +794,7 @@ class TestMigrations(TestCase): # parsing right if we get a lot of data. anomaly = anomaly_domain_infos.get() self.assertEqual(anomaly.organization_name, "Flashdog") - self.assertEqual(anomaly.organization_type, None) + self.assertEqual(anomaly.generic_org_type, None) self.assertEqual(anomaly.federal_agency, None) self.assertEqual(anomaly.federal_type, None) @@ -809,7 +809,7 @@ class TestMigrations(TestCase): fakewebsite = fakewebsite_domain_infos.get() self.assertEqual(fakewebsite.organization_name, "Fanoodle") - self.assertEqual(fakewebsite.organization_type, "federal") + self.assertEqual(fakewebsite.generic_org_type, "federal") self.assertEqual(fakewebsite.federal_agency, "Department of Commerce") self.assertEqual(fakewebsite.federal_type, "executive") diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index 1414d4525..a4cb210bc 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -57,7 +57,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertEqual(detail_page.status_code, 302) # You can access the 'Location' header to get the redirect URL redirect_url = detail_page.url - self.assertEqual(redirect_url, "/request/organization_type/") + self.assertEqual(redirect_url, "/request/generic_org_type/") def test_domain_request_form_empty_submit(self): """Tests empty submit on the first page after the acknowledgement page""" @@ -141,13 +141,13 @@ class DomainRequestTests(TestWithUser, WebTest): # ---- TYPE PAGE ---- type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" + type_form["generic_org_type-generic_org_type"] = "federal" # test next button and validate data self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() # should see results in db domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.organization_type, "federal") + self.assertEqual(domain_request.generic_org_type, "federal") # the post request should return a redirect to the next form in # the domain request page self.assertEqual(type_result.status_code, 302) @@ -522,7 +522,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertNotContains(type_page, self.TITLES["organization_federal"]) self.assertNotContains(type_page, self.TITLES["organization_election"]) type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" + type_form["generic_org_type-generic_org_type"] = "federal" # set the session ID before .submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -577,7 +577,7 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertNotContains(type_page, self.TITLES["organization_federal"]) self.assertNotContains(type_page, self.TITLES["organization_election"]) type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "county" + type_form["generic_org_type-generic_org_type"] = "county" # set the session ID before .submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -626,7 +626,7 @@ class DomainRequestTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" + type_form["generic_org_type-generic_org_type"] = "federal" self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() @@ -636,7 +636,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Now on federal type page, click back to the organization type self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0) + new_page = federal_page.click(str(self.TITLES["generic_org_type"]), index=0) # Should be a link to the organization_federal page self.assertGreater( @@ -663,7 +663,7 @@ class DomainRequestTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainRequest.OrganizationChoices.INTERSTATE + type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.INTERSTATE self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() @@ -708,7 +708,7 @@ class DomainRequestTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT + type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_page.forms[0].submit() # follow first redirect @@ -884,7 +884,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(555) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1018,7 +1018,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1092,7 +1092,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1169,7 +1169,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1245,7 +1245,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1320,7 +1320,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5556", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1392,7 +1392,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5555", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1458,7 +1458,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5555", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1529,7 +1529,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5555", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1594,7 +1594,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(201) 555 5555", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1667,7 +1667,7 @@ class DomainRequestTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainRequest.OrganizationChoices.INTERSTATE + type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.INTERSTATE self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() # follow first redirect @@ -1695,7 +1695,7 @@ class DomainRequestTests(TestWithUser, WebTest): session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainRequest.OrganizationChoices.TRIBAL + type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.TRIBAL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) type_result = type_form.submit() # the tribal government page comes immediately afterwards @@ -1726,7 +1726,7 @@ class DomainRequestTests(TestWithUser, WebTest): # ---- TYPE PAGE ---- type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" + type_form["generic_org_type-generic_org_type"] = "federal" # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1766,9 +1766,9 @@ class DomainRequestTests(TestWithUser, WebTest): # Go back to organization type page and change type self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page.click(str(self.TITLES["organization_type"]), index=0) + ao_page.click(str(self.TITLES["generic_org_type"]), index=0) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" + type_form["generic_org_type-generic_org_type"] = "city" type_result = type_form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) election_page = type_result.follow() @@ -1797,7 +1797,7 @@ class DomainRequestTests(TestWithUser, WebTest): # ---- TYPE PAGE ---- type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" + type_form["generic_org_type-generic_org_type"] = "federal" # test next button self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) @@ -1866,9 +1866,9 @@ class DomainRequestTests(TestWithUser, WebTest): # Go back to organization type page and change type self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page.click(str(self.TITLES["organization_type"]), index=0) + dotgov_page.click(str(self.TITLES["generic_org_type"]), index=0) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" + type_form["generic_org_type-generic_org_type"] = "city" type_result = type_form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) election_page = type_result.follow() @@ -1936,7 +1936,7 @@ class DomainRequestTests(TestWithUser, WebTest): phone="(555) 555 5557", ) domain_request, _ = DomainRequest.objects.get_or_create( - organization_type="federal", + generic_org_type="federal", federal_type="executive", purpose="Purpose of the site", anything_else="No", @@ -1965,10 +1965,10 @@ class DomainRequestTests(TestWithUser, WebTest): # -- the best that can/should be done here is to ensure the correct values # are being passed to the templating engine - url = reverse("domain-request:organization_type") + url = reverse("domain-request:generic_org_type") response = self.client.get(url, follow=True) self.assertContains(response, "") - # choices = response.context['wizard']['form']['organization_type'].subwidgets + # choices = response.context['wizard']['form']['generic_org_type'].subwidgets # radio = [ x for x in choices if x.data["value"] == "federal" ][0] # checked = radio.data["selected"] # self.assertTrue(checked) diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py index 78f4de83d..064c5efdb 100644 --- a/src/registrar/tests/test_views_domain.py +++ b/src/registrar/tests/test_views_domain.py @@ -665,6 +665,22 @@ class TestDomainManagers(TestDomainOverview): with self.assertRaises(DomainInvitation.DoesNotExist): DomainInvitation.objects.get(id=invitation.id) + def test_domain_invitation_cancel_retrieved_invitation(self): + """Posting to the delete view when invitation retrieved returns an error message""" + email_address = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create( + domain=self.domain, email=email_address, status=DomainInvitation.DomainInvitationStatus.RETRIEVED + ) + with less_console_noise(): + response = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}), follow=True) + # Assert that an error message is displayed to the user + self.assertContains(response, f"Invitation to {email_address} has already been retrieved.") + # Assert that the Cancel link is not displayed + self.assertNotContains(response, "Cancel") + # Assert that the DomainInvitation is not deleted + self.assertTrue(DomainInvitation.objects.filter(id=invitation.id).exists()) + DomainInvitation.objects.filter(email=email_address).delete() + def test_domain_invitation_cancel_no_permissions(self): """Posting to the delete view as a different user should fail.""" email_address = "mayor@igorville.gov" @@ -1051,7 +1067,7 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): """Tests that no edit can occur when the underlying domain is federal""" # Set the org type to federal - self.domain_information.organization_type = DomainInformation.OrganizationChoices.FEDERAL + self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.FEDERAL self.domain_information.save() # Add an AO. We can do this at the model level, just not the form level. @@ -1107,7 +1123,7 @@ class TestDomainAuthorizingOfficial(TestDomainOverview): """Tests that no edit can occur when the underlying domain is tribal""" # Set the org type to federal - self.domain_information.organization_type = DomainInformation.OrganizationChoices.TRIBAL + self.domain_information.generic_org_type = DomainInformation.OrganizationChoices.TRIBAL self.domain_information.save() # Add an AO. We can do this at the model level, just not the form level. @@ -1233,7 +1249,7 @@ class TestDomainOrganization(TestDomainOverview): # Set the current domain to a tribal organization with a preset value. # Save first, so we can test if saving is unaffected (it should be). tribal_org_type = DomainInformation.OrganizationChoices.TRIBAL - self.domain_information.organization_type = tribal_org_type + self.domain_information.generic_org_type = tribal_org_type self.domain_information.save() try: # Add an org name @@ -1242,7 +1258,7 @@ class TestDomainOrganization(TestDomainOverview): except ValueError as err: self.fail(f"A ValueError was caught during the test: {err}") - self.assertEqual(self.domain_information.organization_type, tribal_org_type) + self.assertEqual(self.domain_information.generic_org_type, tribal_org_type) org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) @@ -1290,7 +1306,7 @@ class TestDomainOrganization(TestDomainOverview): # Set the current domain to a tribal organization with a preset value. # Save first, so we can test if saving is unaffected (it should be). fed_org_type = DomainInformation.OrganizationChoices.FEDERAL - self.domain_information.organization_type = fed_org_type + self.domain_information.generic_org_type = fed_org_type self.domain_information.save() try: self.domain_information.federal_agency = "AMTRAK" @@ -1298,7 +1314,7 @@ class TestDomainOrganization(TestDomainOverview): except ValueError as err: self.fail(f"A ValueError was caught during the test: {err}") - self.assertEqual(self.domain_information.organization_type, fed_org_type) + self.assertEqual(self.domain_information.generic_org_type, fed_org_type) org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) @@ -1346,7 +1362,7 @@ class TestDomainOrganization(TestDomainOverview): # Set the current domain to a tribal organization with a preset value. # Save first, so we can test if saving is unaffected (it should be). federal_org_type = DomainInformation.OrganizationChoices.FEDERAL - self.domain_information.organization_type = federal_org_type + self.domain_information.generic_org_type = federal_org_type self.domain_information.save() old_federal_agency_value = ("AMTRAK", "AMTRAK") @@ -1357,7 +1373,7 @@ class TestDomainOrganization(TestDomainOverview): except ValueError as err: self.fail(f"A ValueError was caught during the test: {err}") - self.assertEqual(self.domain_information.organization_type, federal_org_type) + self.assertEqual(self.domain_information.generic_org_type, federal_org_type) new_value = ("Department of State", "Department of State") self.client.post( diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 916e0b0c8..7fc710827 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -80,10 +80,10 @@ def parse_domain_row(columns, domain_info: DomainInformation, security_emails_di if security_email.lower() in invalid_emails: security_email = "(blank)" - if domain_info.federal_type: - domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}" + if domain_info.federal_type and domain_info.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL: + domain_type = f"{domain_info.get_generic_org_type_display()} - {domain_info.get_federal_type_display()}" else: - domain_type = domain_info.get_organization_type_display() + domain_type = domain_info.get_generic_org_type_display() # create a dictionary of fields which can be included in output FIELDS = { @@ -217,9 +217,9 @@ def parse_request_row(columns, request: DomainRequest): requested_domain_name = request.requested_domain.name if request.federal_type: - request_type = f"{request.get_organization_type_display()} - {request.get_federal_type_display()}" + request_type = f"{request.get_generic_org_type_display()} - {request.get_federal_type_display()}" else: - request_type = request.get_organization_type_display() + request_type = request.get_generic_org_type_display() # create a dictionary of fields which can be included in output FIELDS = { @@ -254,6 +254,7 @@ def write_requests_csv( # Reduce the memory overhead when performing the write operation paginator = Paginator(all_requests, 1000) + total_body_rows = [] for page_num in paginator.page_range: page = paginator.page(page_num) @@ -267,10 +268,11 @@ def write_requests_csv( # It indicates that DomainInformation.domain is None. logger.error("csv_export -> Error when parsing row, domain was None") continue + total_body_rows.extend(rows) if should_write_header: write_header(writer, columns) - writer.writerows(rows) + writer.writerows(total_body_rows) def export_data_type_to_csv(csv_file): @@ -295,7 +297,7 @@ def export_data_type_to_csv(csv_file): # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ - "organization_type", + "generic_org_type", Coalesce("federal_type", Value("ZZZZZ")), "federal_agency", "domain__name", @@ -328,7 +330,7 @@ def export_data_full_to_csv(csv_file): ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ - "organization_type", + "generic_org_type", Coalesce("federal_type", Value("ZZZZZ")), "federal_agency", "domain__name", @@ -361,13 +363,13 @@ def export_data_federal_to_csv(csv_file): ] # Coalesce is used to replace federal_type of None with ZZZZZ sort_fields = [ - "organization_type", + "generic_org_type", Coalesce("federal_type", Value("ZZZZZ")), "federal_agency", "domain__name", ] filter_condition = { - "organization_type__icontains": "federal", + "generic_org_type__icontains": "federal", "domain__state__in": [ Domain.State.READY, Domain.State.DNS_NEEDED, @@ -467,24 +469,51 @@ def get_sliced_domains(filter_condition, distinct=False): domains_count = DomainInformation.objects.filter(**filter_condition).distinct().count() # Round trip 2: Get counts for other slices + # This will require either 8 filterd and distinct DB round trips, + # or 2 DB round trips plus iteration on domain_permissions for each domain if distinct: - organization_types_query = ( - DomainInformation.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct() + generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list( + "domain_id", "generic_org_type" ) - else: - organization_types_query = DomainInformation.objects.filter(**filter_condition).values_list( - "organization_type", flat=True - ) - organization_type_counts = Counter(organization_types_query) + # Initialize Counter to store counts for each generic_org_type + generic_org_type_counts = Counter() - federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0) - interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0) - state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0) - tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0) - county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0) - city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0) - special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0) - school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0) + # Keep track of domains already counted + domains_counted = set() + + # Iterate over distinct domains + for domain_id, generic_org_type in generic_org_types_query: + # Check if the domain has already been counted + if domain_id in domains_counted: + continue + + # Get all permissions for the current domain + domain_permissions = DomainInformation.objects.filter(domain_id=domain_id, **filter_condition).values_list( + "domain__permissions", flat=True + ) + + # Check if the domain has multiple permissions + if len(domain_permissions) > 0: + # Mark the domain as counted + domains_counted.add(domain_id) + + # Increment the count for the corresponding generic_org_type + generic_org_type_counts[generic_org_type] += 1 + else: + generic_org_types_query = DomainInformation.objects.filter(**filter_condition).values_list( + "generic_org_type", flat=True + ) + generic_org_type_counts = Counter(generic_org_types_query) + + # Extract counts for each generic_org_type + federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0) + interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0) + state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0) + tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0) + county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0) + city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0) + special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0) + school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0) # Round trip 3 election_board = DomainInformation.objects.filter(is_election_board=True, **filter_condition).distinct().count() @@ -503,31 +532,26 @@ def get_sliced_domains(filter_condition, distinct=False): ] -def get_sliced_requests(filter_condition, distinct=False): +def get_sliced_requests(filter_condition): """Get filtered requests counts sliced by org type and election office.""" # Round trip 1: Get distinct requests based on filter condition requests_count = DomainRequest.objects.filter(**filter_condition).distinct().count() # Round trip 2: Get counts for other slices - if distinct: - organization_types_query = ( - DomainRequest.objects.filter(**filter_condition).values_list("organization_type", flat=True).distinct() - ) - else: - organization_types_query = DomainRequest.objects.filter(**filter_condition).values_list( - "organization_type", flat=True - ) - organization_type_counts = Counter(organization_types_query) + generic_org_types_query = DomainRequest.objects.filter(**filter_condition).values_list( + "generic_org_type", flat=True + ) + generic_org_type_counts = Counter(generic_org_types_query) - federal = organization_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0) - interstate = organization_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0) - state_or_territory = organization_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0) - tribal = organization_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0) - county = organization_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0) - city = organization_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0) - special_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0) - school_district = organization_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0) + federal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.FEDERAL, 0) + interstate = generic_org_type_counts.get(DomainRequest.OrganizationChoices.INTERSTATE, 0) + state_or_territory = generic_org_type_counts.get(DomainRequest.OrganizationChoices.STATE_OR_TERRITORY, 0) + tribal = generic_org_type_counts.get(DomainRequest.OrganizationChoices.TRIBAL, 0) + county = generic_org_type_counts.get(DomainRequest.OrganizationChoices.COUNTY, 0) + city = generic_org_type_counts.get(DomainRequest.OrganizationChoices.CITY, 0) + special_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SPECIAL_DISTRICT, 0) + school_district = generic_org_type_counts.get(DomainRequest.OrganizationChoices.SCHOOL_DISTRICT, 0) # Round trip 3 election_board = DomainRequest.objects.filter(is_election_board=True, **filter_condition).distinct().count() diff --git a/src/registrar/utility/errors.py b/src/registrar/utility/errors.py index 00c65ce57..f00c59bd0 100644 --- a/src/registrar/utility/errors.py +++ b/src/registrar/utility/errors.py @@ -78,15 +78,17 @@ class FSMErrorCodes(IntEnum): - 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 + - 5 NO_REJECTION_REASON No rejection reason is specified """ APPROVE_DOMAIN_IN_USE = 1 NO_INVESTIGATOR = 2 INVESTIGATOR_NOT_STAFF = 3 INVESTIGATOR_NOT_SUBMITTER = 4 + NO_REJECTION_REASON = 5 -class FSMApplicationError(Exception): +class FSMDomainRequestError(Exception): """ Used to raise exceptions when doing FSM Transitions. Uses `FSMErrorCodes` as an enum. @@ -97,6 +99,7 @@ class FSMApplicationError(Exception): 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."), + FSMErrorCodes.NO_REJECTION_REASON: ("A rejection reason is required."), } def __init__(self, *args, code=None, **kwargs): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b2ae76852..9134080a1 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -236,7 +236,7 @@ class DomainAuthorizingOfficialView(DomainFormBaseView): domain_info = self.get_domain_info_from_domain() invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL] - is_federal_or_tribal = domain_info and (domain_info.organization_type in invalid_fields) + is_federal_or_tribal = domain_info and (domain_info.generic_org_type in invalid_fields) form_kwargs["disable_fields"] = is_federal_or_tribal return form_kwargs @@ -244,7 +244,7 @@ class DomainAuthorizingOfficialView(DomainFormBaseView): def get_context_data(self, **kwargs): """Adds custom context.""" context = super().get_context_data(**kwargs) - context["organization_type"] = self.object.domain_info.organization_type + context["generic_org_type"] = self.object.domain_info.generic_org_type return context def get_success_url(self): @@ -822,6 +822,18 @@ class DomainAddUserView(DomainFormBaseView): class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView): object: DomainInvitation # workaround for type mismatch in DeleteView + def post(self, request, *args, **kwargs): + """Override post method in order to error in the case when the + domain invitation status is RETRIEVED""" + self.object = self.get_object() + form = self.get_form() + if form.is_valid() and self.object.status == self.object.DomainInvitationStatus.INVITED: + return self.form_valid(form) + else: + # Produce an error message if the domain invatation status is RETRIEVED + messages.error(request, f"Invitation to {self.object.email} has already been retrieved.") + return HttpResponseRedirect(self.get_success_url()) + def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.domain.id}) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index da5d717f4..244b47602 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -33,7 +33,7 @@ class Step(StrEnum): appear in the order they are defined. (Order matters.) """ - ORGANIZATION_TYPE = "organization_type" + ORGANIZATION_TYPE = "generic_org_type" TRIBAL_GOVERNMENT = "tribal_government" ORGANIZATION_FEDERAL = "organization_federal" ORGANIZATION_ELECTION = "organization_election" @@ -340,7 +340,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): Queries the DB for a domain request and returns a list of unlocked steps.""" history_dict = { - "organization_type": self.domain_request.organization_type is not None, + "generic_org_type": self.domain_request.generic_org_type is not None, "tribal_government": self.domain_request.tribe_name is not None, "organization_federal": self.domain_request.federal_type is not None, "organization_election": self.domain_request.is_election_board is not None, @@ -506,7 +506,7 @@ class AuthorizingOfficial(DomainRequestWizard): def get_context_data(self): context = super().get_context_data() - context["organization_type"] = self.domain_request.organization_type + context["generic_org_type"] = self.domain_request.generic_org_type context["federal_type"] = self.domain_request.federal_type return context @@ -522,7 +522,7 @@ class DotgovDomain(DomainRequestWizard): def get_context_data(self): context = super().get_context_data() - context["organization_type"] = self.domain_request.organization_type + context["generic_org_type"] = self.domain_request.generic_org_type context["federal_type"] = self.domain_request.federal_type return context