diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index fe02d389f..d8d61ac87 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -27,6 +27,7 @@ for step, view in [ (Step.ORGANIZATION_FEDERAL, views.OrganizationFederal), (Step.ORGANIZATION_ELECTION, views.OrganizationElection), (Step.ORGANIZATION_CONTACT, views.OrganizationContact), + (Step.TYPE_OF_WORK, views.TypeOfWork), (Step.AUTHORIZING_OFFICIAL, views.AuthorizingOfficial), (Step.CURRENT_SITES, views.CurrentSites), (Step.DOTGOV_DOMAIN, views.DotgovDomain), diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 28118ed53..6059d1a6f 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -151,7 +151,7 @@ class OrganizationContactForm(RegistrarForm): raise forms.ValidationError( "Please select your federal agency.", code="required" ) - if self.application.is_federal: + if self.application.is_federal(): if not federal_agency: # no answer was selected raise forms.ValidationError( @@ -160,6 +160,17 @@ class OrganizationContactForm(RegistrarForm): return federal_agency +class TypeOfWorkForm(RegistrarForm): + type_of_work = forms.CharField( + label="What type of work does your organization do?", widget=forms.Textarea() + ) + + more_organization_information = forms.CharField( + label="Describe how your organization is a government organization that is independent of a state government. Include links to authorizing legislation, applicable bylaws or charter, or other documentation to support your claims.", + widget=forms.Textarea(), + ) + + class AuthorizingOfficialForm(RegistrarForm): def to_database(self, obj): if not self.is_valid(): diff --git a/src/registrar/migrations/0007_domainapplication_more_organization_information_and_more.py b/src/registrar/migrations/0007_domainapplication_more_organization_information_and_more.py new file mode 100644 index 000000000..dfdadc618 --- /dev/null +++ b/src/registrar/migrations/0007_domainapplication_more_organization_information_and_more.py @@ -0,0 +1,131 @@ +# Generated by Django 4.1.5 on 2023-01-10 20:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0006_alter_contact_phone"), + ] + + operations = [ + migrations.AddField( + model_name="domainapplication", + name="more_organization_information", + field=models.TextField( + blank=True, + help_text="Further information about the government organization", + null=True, + ), + ), + migrations.AddField( + model_name="domainapplication", + name="type_of_work", + field=models.TextField( + blank=True, help_text="Type of work of the organization", null=True + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="address_line1", + field=models.TextField(blank=True, help_text="Street address", null=True), + ), + migrations.AlterField( + model_name="domainapplication", + name="address_line2", + field=models.CharField( + blank=True, help_text="Street address line 2", max_length=15, null=True + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="federal_agency", + field=models.TextField(blank=True, help_text="Federal agency", null=True), + ), + migrations.AlterField( + model_name="domainapplication", + name="federal_type", + field=models.CharField( + blank=True, + choices=[ + ("executive", "Executive"), + ("judicial", "Judicial"), + ("legislative", "Legislative"), + ], + help_text="Federal government branch", + max_length=50, + null=True, + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="organization_type", + field=models.CharField( + blank=True, + choices=[ + ( + "federal", + "Federal: an agency of the U.S. government's executive, legislative, or judicial branches", + ), + ("interstate", "Interstate: an organization of two or more states"), + ( + "state_or_territory", + "State or territory: one of the 50 U.S. states, the District of Columbia, American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. Virgin Islands", + ), + ( + "tribal", + "Tribal: a tribal government recognized by the federal or a state government", + ), + ("county", "County: a county, parish, or borough"), + ("city", "City: a city, town, township, village, etc."), + ( + "special_district", + "Special district: an independent organization within a single state", + ), + ( + "school_district", + "School district: a school district that is not part of a local government", + ), + ], + help_text="Type of Organization", + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="purpose", + field=models.TextField( + blank=True, help_text="Purpose of your domain", null=True + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="state_territory", + field=models.CharField( + blank=True, + help_text="State, territory, or military post", + max_length=2, + null=True, + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="urbanization", + field=models.TextField( + blank=True, help_text="Urbanization (Puerto Rico only)", null=True + ), + ), + migrations.AlterField( + model_name="domainapplication", + name="zipcode", + field=models.CharField( + blank=True, + db_index=True, + help_text="Zip code", + max_length=10, + null=True, + ), + ), + ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index ab59192ac..6218a1ecc 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -350,6 +350,18 @@ class DomainApplication(TimeStampedModel): help_text="Urbanization (Puerto Rico only)", ) + type_of_work = models.TextField( + null=True, + blank=True, + help_text="Type of work of the organization", + ) + + more_organization_information = models.TextField( + null=True, + blank=True, + help_text="Further information about the government organization", + ) + authorizing_official = models.ForeignKey( "registrar.Contact", null=True, @@ -474,6 +486,14 @@ class DomainApplication(TimeStampedModel): ] return bool(user_choice and user_choice not in excluded) + def show_type_of_work(self) -> bool: + """Show this step if this is a special district or interstate.""" + user_choice = self.organization_type + return user_choice in [ + DomainApplication.OrganizationChoices.SPECIAL_DISTRICT, + DomainApplication.OrganizationChoices.INTERSTATE, + ] + def is_federal(self) -> Union[bool, None]: """Is this application for a federal agency? diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index 5b3ce7e3b..2c8bf874c 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -30,6 +30,10 @@ Incomplete {% endif %} {% endif %} + {% if step == Step.TYPE_OF_WORK %} +

{{ application.type_of_work|default:"Incomplete" }}

+

{{ application.more_organization_information|default:"Incomplete" }}

+ {% endif %} {% if step == Step.AUTHORIZING_OFFICIAL %} {% if application.authorizing_official %} {% include "includes/contact.html" with contact=application.authorizing_official %} diff --git a/src/registrar/templates/application_type_of_work.html b/src/registrar/templates/application_type_of_work.html new file mode 100644 index 000000000..8bc0ddd6e --- /dev/null +++ b/src/registrar/templates/application_type_of_work.html @@ -0,0 +1,57 @@ + +{% extends 'application_form.html' %} +{% load widget_tweaks %} + +{% block form_content %} + +
+
+ {% csrf_token %} + +
+ {% with field=forms.0.type_of_work %} + {% if field.errors %} +
+ {{ field|add_label_class:"usa-label usa-label--error" }} + {% for error in field.errors %} + + {{ error }} + + {% endfor %} + {{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500"|attr:"aria-invalid:true" }} +
+ {% else %} + {{ field|add_label_class:"usa-label" }} + {{ field|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }} + {% endif %} + {% endwith %} + You can enter up to 500 characters +
+
+ +
+
+ {% with field=forms.0.more_organization_information %} + {% if field.errors %} +
+ {{ field|add_label_class:"usa-label usa-label--error" }} + {% for error in field.errors %} + + {{ error }} + + {% endfor %} + {{ field|add_class:"usa-input--error usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500"|attr:"aria-invalid:true" }} +
+ {% else %} + {{ field|add_label_class:"usa-label" }} + {{ field|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }} + {% endif %} + {% endwith %} + You can enter up to 500 characters +
+
+ {{ block.super }} + +
+ +{% endblock %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index f511a0f33..d6e4f5035 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model from django_webtest import WebTest # type: ignore from registrar.models import DomainApplication, Domain, Contact, Website -from registrar.views.application import ApplicationWizard +from registrar.views.application import ApplicationWizard, Step from .common import less_console_noise @@ -120,7 +120,7 @@ class DomainApplicationTests(TestWithUser, WebTest): this test work. """ num_pages_tested = 0 - SKIPPED_PAGES = 1 # elections + SKIPPED_PAGES = 2 # elections, type_of_work num_pages = len(self.TITLES) - SKIPPED_PAGES type_page = self.app.get(reverse("application:")).follow() @@ -660,6 +660,82 @@ class DomainApplicationTests(TestWithUser, WebTest): 0, ) + def test_application_form_nonfederal(self): + """Non-federal organizations don't have to provide their federal agency.""" + type_page = self.app.get(reverse("application:")).follow() + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.form + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_page.form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + org_contact_form = contact_page.form + + self.assertNotIn("federal_agency", org_contact_form.fields) + + # minimal fields that must be filled out + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_result = org_contact_form.submit() + + # the post request should return a redirect to the type of work page + # if it was successful. + self.assertEquals(contact_result.status_code, 302) + self.assertEquals(contact_result["Location"], "/register/type_of_work/") + + + def test_application_type_of_work_special(self): + """Special districts have to answer an additional question.""" + type_page = self.app.get(reverse("application:")).follow() + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.form + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_page.form.submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.TYPE_OF_WORK]) + + def test_application_type_of_work_interstate(self): + """Special districts have to answer an additional question.""" + type_page = self.app.get(reverse("application:")).follow() + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.form + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_page.form.submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.TYPE_OF_WORK]) + + @skip("WIP") def test_application_edit_restore(self): """ diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 713e9261b..3d210d862 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -27,6 +27,7 @@ class Step(StrEnum): ORGANIZATION_FEDERAL = "organization_federal" ORGANIZATION_ELECTION = "organization_election" ORGANIZATION_CONTACT = "organization_contact" + TYPE_OF_WORK = "type_of_work" AUTHORIZING_OFFICIAL = "authorizing_official" CURRENT_SITES = "current_sites" DOTGOV_DOMAIN = "dotgov_domain" @@ -70,6 +71,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): Step.ORGANIZATION_FEDERAL: _("Type of organization — Federal"), Step.ORGANIZATION_ELECTION: _("Type of organization — Election board"), Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"), + Step.TYPE_OF_WORK: _("Type of Work"), Step.AUTHORIZING_OFFICIAL: _("Authorizing official"), Step.CURRENT_SITES: _("Organization website"), Step.DOTGOV_DOMAIN: _(".gov domain"), @@ -93,6 +95,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): Step.ORGANIZATION_ELECTION: lambda w: w.from_model( "show_organization_election", False ), + Step.TYPE_OF_WORK: lambda w: w.from_model("show_type_of_work", False), } def __init__(self): @@ -353,6 +356,11 @@ class OrganizationContact(ApplicationWizard): return context +class TypeOfWork(ApplicationWizard): + template_name = "application_type_of_work.html" + forms = [forms.TypeOfWorkForm] + + class AuthorizingOfficial(ApplicationWizard): template_name = "application_authorizing_official.html" forms = [forms.AuthorizingOfficialForm]