diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index a19da4791..0ef9fbe36 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -88,6 +88,11 @@ urlpatterns = [ views.DomainYourContactInformationView.as_view(), name="domain-your-contact-information", ), + path( + "domain//org-name-address", + views.DomainOrgNameAddressView.as_view(), + name="domain-org-name-address", + ), path( "domain//authorizing-official", views.DomainAuthorizingOfficialView.as_view(), diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 364740211..13f75563f 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -3,5 +3,6 @@ from .domain import ( DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm, + DomainOrgNameAddressForm, ContactForm, ) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index e6fbc8fee..f14448bcf 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -1,11 +1,12 @@ """Forms for domain management.""" from django import forms +from django.core.validators import RegexValidator from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget -from ..models import Contact +from ..models import Contact, DomainInformation class DomainAddUserForm(forms.Form): @@ -64,3 +65,77 @@ class DomainSecurityEmailForm(forms.Form): """Form for adding or editing a security email to a domain.""" security_email = forms.EmailField(label="Security email") + + +class DomainOrgNameAddressForm(forms.ModelForm): + + """Form for updating the organization name and mailing address.""" + + zipcode = forms.CharField( + label="Zip code", + validators=[ + RegexValidator( + "^[0-9]{5}(?:-[0-9]{4})?$|^$", + message="Enter a zip code in the form of 12345 or 12345-6789.", + ) + ], + ) + + class Meta: + model = DomainInformation + fields = [ + "federal_agency", + "organization_name", + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + "urbanization", + ] + error_messages = { + "federal_agency": { + "required": "Select the federal agency for your organization." + }, + "organization_name": {"required": "Enter the name of your organization."}, + "address_line1": { + "required": "Enter the street address of your organization." + }, + "city": {"required": "Enter the city where your organization is located."}, + "state_territory": { + "required": "Select the state, territory, or military post where your" + "organization is located." + }, + } + widgets = { + # We need to set the required attributed for federal_agency and + # state/territory because for these fields we are creating an individual + # instance of the Select. For the other fields we use the for loop to set + # the class's required attribute to true. + "federal_agency": forms.Select( + attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES + ), + "organization_name": forms.TextInput, + "address_line1": forms.TextInput, + "address_line2": forms.TextInput, + "city": forms.TextInput, + "state_territory": forms.Select( + attrs={ + "required": True, + }, + choices=DomainInformation.StateTerritoryChoices.choices, + ), + "urbanization": forms.TextInput, + } + + # the database fields have blank=True so ModelForm doesn't create + # required fields by default. Use this list in __init__ to mark each + # of these fields as required + required = ["organization_name", "address_line1", "city", "zipcode"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name in self.required: + self.fields[field_name].required = True + self.fields["state_territory"].widget.attrs.pop("maxlength", None) + self.fields["zipcode"].widget.attrs.pop("maxlength", None) diff --git a/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py b/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py new file mode 100644 index 000000000..9f362c956 --- /dev/null +++ b/src/registrar/migrations/0027_alter_domaininformation_address_line1_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.1 on 2023-06-09 16:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0026_alter_domainapplication_address_line2_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="domaininformation", + name="address_line1", + field=models.TextField( + blank=True, + help_text="Street address", + null=True, + verbose_name="Street address", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="address_line2", + field=models.TextField( + blank=True, + help_text="Street address line 2", + null=True, + verbose_name="Street address line 2", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="state_territory", + field=models.CharField( + blank=True, + help_text="State, territory, or military post", + max_length=2, + null=True, + verbose_name="State, territory, or military post", + ), + ), + migrations.AlterField( + model_name="domaininformation", + name="urbanization", + field=models.TextField( + blank=True, + help_text="Urbanization (Puerto Rico only)", + null=True, + verbose_name="Urbanization (Puerto Rico only)", + ), + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 78b68a3fa..b12039e73 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -100,11 +100,13 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Street address", + verbose_name="Street address", ) address_line2 = models.TextField( null=True, blank=True, help_text="Street address line 2", + verbose_name="Street address line 2", ) city = models.TextField( null=True, @@ -116,6 +118,7 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="State, territory, or military post", + verbose_name="State, territory, or military post", ) zipcode = models.CharField( max_length=10, @@ -128,6 +131,7 @@ class DomainInformation(TimeStampedModel): null=True, blank=True, help_text="Urbanization (Puerto Rico only)", + verbose_name="Urbanization (Puerto Rico only)", ) type_of_work = models.TextField( diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index ef3484dfc..dd176c862 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -14,7 +14,7 @@ Add DNS name servers {% endif %} - {% url 'todo' as url %} + {% url 'domain-org-name-address' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %} {% url 'domain-authorizing-official' pk=domain.id as url %} diff --git a/src/registrar/templates/domain_org_name_address.html b/src/registrar/templates/domain_org_name_address.html new file mode 100644 index 000000000..587ba4782 --- /dev/null +++ b/src/registrar/templates/domain_org_name_address.html @@ -0,0 +1,47 @@ +{% extends "domain_base.html" %} +{% load static field_helpers%} + +{% block title %}Organization name and mailing address | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + {# this is right after the messages block in the parent template #} + {% include "includes/form_errors.html" with form=form %} + +

Organization name and mailing address

+ +

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

+ + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + + {% if domain.domain_info.organization_type == 'federal' %} + {% input_with_errors form.federal_agency %} + {% endif %} + + {% input_with_errors form.organization_name %} + + {% input_with_errors form.address_line1 %} + + {% input_with_errors form.address_line2 %} + + {% input_with_errors form.city %} + + {% input_with_errors form.state_territory %} + + {% with add_class="usa-input--small" %} + {% input_with_errors form.zipcode %} + {% endwith %} + + {% input_with_errors form.urbanization %} + + +
+ +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 5b0457e55..1e4cd1882 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -22,7 +22,7 @@
  • - {% url 'todo' as url %} + {% url 'domain-org-name-address' pk=domain.id as url %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 500912952..ffe4cb671 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1057,6 +1057,7 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain-users", "domain-users-add", "domain-nameservers", + "domain-org-name-address", "domain-authorizing-official", "domain-your-contact-information", "domain-security-email", @@ -1077,6 +1078,7 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain-users", "domain-users-add", "domain-nameservers", + "domain-org-name-address", "domain-authorizing-official", "domain-your-contact-information", "domain-security-email", @@ -1314,6 +1316,42 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): ) self.assertContains(page, "Testy") + def test_domain_org_name_address(self): + """Can load domain's org name and mailing address page.""" + page = self.client.get( + reverse("domain-org-name-address", kwargs={"pk": self.domain.id}) + ) + # once on the sidebar, once in the page title, once as H1 + self.assertContains(page, "Organization name and mailing address", count=3) + + def test_domain_org_name_address_content(self): + """Org name and address information appears on the page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + page = self.app.get( + reverse("domain-org-name-address", kwargs={"pk": self.domain.id}) + ) + self.assertContains(page, "Town of Igorville") + + def test_domain_org_name_address_form(self): + """Submitting changes works on the org name address page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + org_name_page = self.app.get( + reverse("domain-org-name-address", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + org_name_page.form["organization_name"] = "Not igorville" + org_name_page.form["city"] = "Faketown" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 200) + + self.assertContains(success_result_page, "Not igorville") + self.assertContains(success_result_page, "Faketown") + def test_domain_your_contact_information(self): """Can load domain's your contact information page.""" page = self.client.get( diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 8212d140d..ed1d8ba39 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -2,6 +2,7 @@ from .application import * from .domain import ( DomainView, DomainAuthorizingOfficialView, + DomainOrgNameAddressView, DomainNameserversView, DomainYourContactInformationView, DomainSecurityEmailView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9771a09c6..6a33ec994 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -22,10 +22,11 @@ from registrar.models import ( ) from ..forms import ( - DomainAddUserForm, - NameserverFormset, - DomainSecurityEmailForm, ContactForm, + DomainOrgNameAddressForm, + DomainAddUserForm, + DomainSecurityEmailForm, + NameserverFormset, ) from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView @@ -41,6 +42,47 @@ class DomainView(DomainPermissionView): template_name = "domain_detail.html" +class DomainOrgNameAddressView(DomainPermissionView, FormMixin): + """Organization name and mailing address view""" + + model = Domain + template_name = "domain_org_name_address.html" + context_object_name = "domain" + form_class = DomainOrgNameAddressForm + + def get_form_kwargs(self, *args, **kwargs): + """Add domain_info.organization_name instance to make a bound form.""" + form_kwargs = super().get_form_kwargs(*args, **kwargs) + form_kwargs["instance"] = self.get_object().domain_info + return form_kwargs + + def get_success_url(self): + """Redirect to the overview page for the domain.""" + return reverse("domain-org-name-address", kwargs={"pk": self.object.pk}) + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using DetailView and FormMixin together. + """ + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """The form is valid, save the organization name and mailing address.""" + form.save() + + messages.success( + self.request, "The organization name and mailing address has been updated." + ) + # superclass has the redirect + return super().form_valid(form) + + class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): """Domain authorizing official editing view."""