Repository: cisagov/manage.get.gov

Issue:
Federal Agencies - Create a Dropdown instead of freeform
AC:

- Update Federal Agency from a free-form field to dropdown
- Open-source code has list
- Create an "Other" field with a free form field

We can use the [federal registrar](https://www.federalregister.gov/developers/documentation/api/v1#/Agencies/get_agencies) for this.


Discussion:
Other examples of agency lists: 
[10x form](https://feedback.gsa.gov/jfe/form/SV_1Im8dTPnjnV3HpP)
[USAJobs search results](https://www.usajobs.gov/Search/Results?l=20009)
Non-hierarchical list https://www.usa.gov/federal-agencies includes cabinet departments and sub-agencies; sub-agencies also appear on agency pages https://www.usa.gov/federal-agencies/u-s-department-of-homeland-security

Resolving patch:
diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py
--- a/src/registrar/forms/application_wizard.py
+++ b/src/registrar/forms/application_wizard.py
@@ -4,6 +4,8 @@
 
 import logging
 
+from typing import Union
+
 from django import forms
 from django.shortcuts import render
 from django.contrib.auth.mixins import LoginRequiredMixin
@@ -69,14 +71,23 @@ class OrganizationElectionForm(RegistrarForm):
 
 
 class OrganizationContactForm(RegistrarForm):
-    organization_name = forms.CharField(label="Organization Name")
+    # for federal agencies we also want to know the top-level agency.
+    federal_agency = forms.ChoiceField(
+        label="Federal agency",
+        # not required because this field won't be filled out unless
+        # it is a federal agency.
+        required=False,
+        choices=DomainApplication.AGENCY_CHOICES,
+    )
+    organization_name = forms.CharField(label="Organization name")
     address_line1 = forms.CharField(label="Address line 1")
     address_line2 = forms.CharField(
         required=False,
         label="Address line 2",
     )
     state_territory = forms.ChoiceField(
-        label="State", choices=DomainApplication.StateTerritoryChoices.choices
+        label="State/territory",
+        choices=[("", "--Select--")] + DomainApplication.StateTerritoryChoices.choices,
     )
     zipcode = forms.CharField(label="ZIP code")
 
@@ -389,10 +400,21 @@ def get_template_names(self):
         """
         return [TEMPLATES[self.steps.current]]
 
+    def _is_federal(self) -> Union[bool, None]:
+        """Return whether this application is from a federal agency.
+
+        Returns True if we know that this application is from a federal
+        agency, False if we know that it is not and None if there isn't an
+        answer yet for that question.
+        """
+        return self.get_application_object().is_federal()
+
     def get_context_data(self, form, **kwargs):
         """Add title information to the context for all steps."""
         context = super().get_context_data(form=form, **kwargs)
         context["form_titles"] = TITLES
+        if self.steps.current == Step.ORGANIZATION_CONTACT:
+            context["is_federal"] = self._is_federal()
         return context
 
     def get_application_object(self) -> DomainApplication:
diff --git a/src/registrar/migrations/0004_domainapplication_federal_agency.py b/src/registrar/migrations/0004_domainapplication_federal_agency.py
new file mode 100644
--- /dev/null
+++ b/src/registrar/migrations/0004_domainapplication_federal_agency.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.1.3 on 2022-12-07 15:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        (
+            "registrar",
+            "0003_rename_is_election_office_domainapplication_is_election_board_and_more",
+        ),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="domainapplication",
+            name="federal_agency",
+            field=models.TextField(help_text="Top level federal agency", null=True),
+        ),
+    ]
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -108,6 +108,146 @@ class BranchChoices(models.TextChoices):
         JUDICIAL = "judicial", "Judicial"
         LEGISLATIVE = "legislative", "Legislative"
 
+    AGENCIES = [
+        "",
+        "Administrative Conference of the United States",
+        "Advisory Council on Historic Preservation",
+        "American Battle Monuments Commission",
+        "Appalachian Regional Commission",
+        (
+            "Appraisal Subcommittee of the Federal Financial "
+            "Institutions Examination Council"
+        ),
+        "Armed Forces Retirement Home",
+        "Barry Goldwater Scholarship and Excellence in Education Program",
+        "Central Intelligence Agency",
+        "Christopher Columbus Fellowship Foundation",
+        "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",
+        "Consumer Financial Protection Bureau",
+        "Consumer Product Safety Commission",
+        "Corporation for National and Community Service",
+        "Council of Inspectors General on Integrity and Efficiency",
+        "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",
+        "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 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 Reserve System",
+        "Federal Trade Commission",
+        "General Services Administration",
+        "Gulf Coast Ecosystem Restoration Council",
+        "Harry S Truman Scholarship Foundation",
+        "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-United States Friendship Commission",
+        "John F. Kennedy Center for the Performing Arts",
+        "Legal Services Corporation",
+        "Legislative Branch",
+        "Marine Mammal Commission",
+        "Medicare Payment Advisory Commission",
+        "Merit Systems Protection Board",
+        "Millennium Challenge Corporation",
+        "National Aeronautics and Space Administration",
+        "National Archives and Records Administration",
+        "National Capital Planning Commission",
+        "National Council on Disability",
+        "National Credit Union Administration",
+        "National Foundation on the Arts and the Humanities",
+        "National Gallery of Art",
+        "National Labor Relations Board",
+        "National Mediation Board",
+        "National Science Foundation",
+        "National Transportation Safety Board",
+        "Northern Border Regional Commission",
+        "Nuclear Regulatory Commission",
+        "Nuclear Safety Oversight Committee",
+        "Nuclear Waste Technical Review Board",
+        "Occupational Safety and Health Review Commission",
+        "Office of Compliance",
+        "Office of Government Ethics",
+        "Office of Navajo and Hopi Indian Relocation",
+        "Office of Personnel Management",
+        "Overseas Private Investment Corporation",
+        "Peace Corps",
+        "Pension Benefit Guaranty Corporation",
+        "Postal Regulatory Commission",
+        "Privacy and Civil Liberties Oversight 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",
+        "State Justice Institute",
+        "State, Local, and Tribal Government",
+        "Stennis Center for Public Service",
+        "Surface Transportation Board",
+        "Tennessee Valley Authority",
+        "The Executive Office of the President",
+        "U.S. Access Board",
+        "U.S. Agency for Global Media",
+        "U.S. Agency for International Development",
+        "U.S. Chemical Safety Board",
+        "U.S. China Economic and Security Review Commission",
+        "U.S. Commission on Civil Rights",
+        "U.S. Commission on International Religious Freedom",
+        "U.S. Interagency Council on Homelessness",
+        "U.S. International Trade Commission",
+        "U.S. Office of Special Counsel",
+        "U.S. Postal Service",
+        "U.S. Trade and Development Agency",
+        "Udall Foundation",
+        "United States African Development Foundation",
+        "United States Arctic Research Commission",
+        "United States Holocaust Memorial Museum",
+        "Utah Reclamation Mitigation and Conservation Commission",
+        "Vietnam Education Foundation",
+        "Woodrow Wilson International Center for Scholars",
+        "World War I Centennial Commission",
+    ]
+    AGENCY_CHOICES = [(v, v) for v in AGENCIES]
+
     # #### Internal fields about the application #####
     status = FSMField(
         choices=STATUS_CHOICES,  # possible states as an array of constants
@@ -138,6 +278,12 @@ class BranchChoices(models.TextChoices):
         help_text="Type of Organization",
     )
 
+    federal_agency = models.TextField(
+        null=True,
+        blank=False,
+        help_text="Top level federal agency",
+    )
+
     federal_type = models.CharField(
         max_length=50,
         choices=BranchChoices.choices,
@@ -319,3 +465,15 @@ def show_organization_election(wizard: ApplicationWizard) -> bool:
             DomainApplication.OrganizationChoices.INTERSTATE,
         ]
         return bool(user_choice and user_choice not in excluded)
+
+    def is_federal(self) -> Union[bool, None]:
+        """Is this application for a federal agency?
+
+        organization_type can be both null and blank,
+        """
+        if not self.organization_type:
+            # organization_type is either blank or None, can't answer
+            return None
+        if self.organization_type == DomainApplication.OrganizationChoices.FEDERAL:
+            return True
+        return False


Test patch:
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -178,6 +178,7 @@ def test_application_form_submission(self):
 
         # ---- ORG CONTACT PAGE  ----
         # Follow the redirect to the next form page
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
         org_contact_page = federal_result.follow()
         org_contact_form = org_contact_page.form
         org_contact_form["organization_contact-organization_name"] = "Testorg"
@@ -517,6 +518,19 @@ def test_application_form_conditional_federal(self):
         self.assertContains(federal_page, TITLES["organization_federal"])
         self.assertNotContains(federal_page, TITLES["organization_election"])
 
+        # continuing on in the flow we need to see top-level agency on the
+        # contact page
+        federal_page.form["organization_federal-federal_type"] = "executive"
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        federal_result = federal_page.form.submit()
+        # the post request should return a redirect to the contact
+        # question
+        self.assertEquals(federal_result.status_code, 302)
+        self.assertEquals(federal_result["Location"], "/register/organization_contact/")
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        contact_page = federal_result.follow()
+        self.assertContains(contact_page, "Federal agency")
+
     def test_application_form_conditional_elections(self):
         """Election question is shown for other organizations."""
         type_page = self.app.get(reverse("application")).follow()
@@ -549,6 +563,21 @@ def test_application_form_conditional_elections(self):
         self.assertContains(election_page, TITLES["organization_election"])
         self.assertNotContains(election_page, TITLES["organization_federal"])
 
+        # continuing on in the flow we need to NOT see top-level agency on the
+        # contact page
+        election_page.form["organization_election-is_election_board"] = "True"
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        election_result = election_page.form.submit()
+        # the post request should return a redirect to the contact
+        # question
+        self.assertEquals(election_result.status_code, 302)
+        self.assertEquals(
+            election_result["Location"], "/register/organization_contact/"
+        )
+        self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+        contact_page = election_result.follow()
+        self.assertNotContains(contact_page, "Federal agency")
+
     @skip("WIP")
     def test_application_edit_restore(self):
         """