diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index a0c71d48c..374b3102f 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -10,3 +10,6 @@ DomainDsdataFormset, DomainDsdataForm, ) +from .portfolio import ( + PortfolioOrgAddressForm, +) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 9b8f1b7fc..02a0724d1 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -458,7 +458,7 @@ class Meta: # 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"] + required = ["organization_name", "address_line1", "city", "state_territory", "zipcode"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py new file mode 100644 index 000000000..9362c7bbd --- /dev/null +++ b/src/registrar/forms/portfolio.py @@ -0,0 +1,69 @@ +"""Forms for portfolio.""" + +import logging +from django import forms +from django.core.validators import RegexValidator + +from ..models import DomainInformation, Portfolio + +logger = logging.getLogger(__name__) + + +class PortfolioOrgAddressForm(forms.ModelForm): + """Form for updating the portfolio org mailing address.""" + + zipcode = forms.CharField( + label="Zip code", + validators=[ + RegexValidator( + "^[0-9]{5}(?:-[0-9]{4})?$|^$", + message="Enter a zip code in the required format, like 12345 or 12345-6789.", + ) + ], + ) + + class Meta: + model = Portfolio + fields = [ + "address_line1", + "address_line2", + "city", + "state_territory", + "zipcode", + # "urbanization", + ] + error_messages = { + "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 State/territory + # because for this 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. + "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 = ["address_line1", "city", "state_territory", "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/models/portfolio.py b/src/registrar/models/portfolio.py index 7d0500e19..06b01e672 100644 --- a/src/registrar/models/portfolio.py +++ b/src/registrar/models/portfolio.py @@ -6,11 +6,6 @@ from .utility.time_stamped_model import TimeStampedModel -# def get_default_federal_agency(): -# """returns non-federal agency""" -# return FederalAgency.objects.filter(agency="Non-Federal Agency").first() - - class Portfolio(TimeStampedModel): """ Portfolio is used for organizing domains/domain-requests into diff --git a/src/registrar/templates/portfolio_organization.html b/src/registrar/templates/portfolio_organization.html index c7eae7130..0dede3c32 100644 --- a/src/registrar/templates/portfolio_organization.html +++ b/src/registrar/templates/portfolio_organization.html @@ -1,8 +1,64 @@ {% extends 'portfolio_base.html' %} +{% load static field_helpers%} + +{% block title %}Organization mailing address | {{ portfolio.name }} | {% endblock %} {% load static %} {% block portfolio_content %} -

Organization

+
+
+

+ Portfolio name: {{ portfolio }} +

+ + {% include 'portfolio_organization_sidebar.html' %} +
+ +
+ +

Organization

+ +

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

+ +

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

+ + {% include "includes/form_errors.html" with form=form %} + + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + +

+ Federal agency + {{ portfolio }} +

+ + {% 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 %} + +
+
+
{% endblock %} diff --git a/src/registrar/templates/portfolio_organization_sidebar.html b/src/registrar/templates/portfolio_organization_sidebar.html new file mode 100644 index 000000000..6199c2476 --- /dev/null +++ b/src/registrar/templates/portfolio_organization_sidebar.html @@ -0,0 +1,23 @@ +{% load static url_helpers %} + +
+ +
diff --git a/src/registrar/tests/test_views_portfolio.py b/src/registrar/tests/test_views_portfolio.py index 3ab2fc2fd..3596bf567 100644 --- a/src/registrar/tests/test_views_portfolio.py +++ b/src/registrar/tests/test_views_portfolio.py @@ -1,5 +1,6 @@ from django.urls import reverse from api.tests.common import less_console_noise_decorator +from registrar.config import settings from registrar.models.portfolio import Portfolio from django_webtest import WebTest # type: ignore from registrar.models import ( @@ -17,7 +18,7 @@ logger = logging.getLogger(__name__) -class TestPortfolioViews(WebTest): +class TestPortfolio(WebTest): def setUp(self): super().setUp() self.user = create_test_user() @@ -192,3 +193,70 @@ def test_navigation_links_hidden_when_user_not_have_permission(self): self.assertNotContains( portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk}) ) + + +class TestPortfolioOrganization(TestPortfolio): + + def test_portfolio_org_name(self): + """Can load portfolio's org name page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + self.assertContains( + page, "The name of your federal agency will be publicly listed as the domain registrant." + ) + + def test_domain_org_name_address_content(self): + """Org name and address information appears on the page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + self.portfolio.organization_name = "Hotel California" + self.portfolio.save() + page = self.app.get(reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk})) + # Once in the sidenav, once in the main nav, once in the form + self.assertContains(page, "Hotel California", count=3) + + def test_domain_org_name_address_form(self): + """Submitting changes works on the org name address page.""" + with override_flag("organization_feature", active=True): + self.app.set_user(self.user.username) + self.user.portfolio = self.portfolio + self.user.portfolio_additional_permissions = [ + User.UserPortfolioPermissionChoices.VIEW_PORTFOLIO, + User.UserPortfolioPermissionChoices.EDIT_PORTFOLIO, + ] + self.user.save() + self.user.refresh_from_db() + + self.portfolio.address_line1 = "1600 Penn Ave" + self.portfolio.save() + portfolio_org_name_page = self.app.get( + reverse("portfolio-organization", kwargs={"portfolio_id": self.portfolio.pk}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + portfolio_org_name_page.form["address_line1"] = "6 Downing st" + portfolio_org_name_page.form["city"] = "London" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = portfolio_org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 200) + + self.assertContains(success_result_page, "6 Downing st") + self.assertContains(success_result_page, "London") diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index f693e2cc9..abd9648ba 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -1,4 +1,8 @@ +import logging from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.contrib import messages +from registrar.forms.portfolio import PortfolioOrgAddressForm from registrar.models.portfolio import Portfolio from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, @@ -7,6 +11,10 @@ ) from waffle.decorators import flag_is_active from django.views.generic import View +from django.views.generic.edit import FormMixin + + +logger = logging.getLogger(__name__) class PortfolioDomainsView(PortfolioDomainsPermissionView, View): @@ -42,17 +50,61 @@ def get(self, request, portfolio_id): return render(request, "portfolio_requests.html", context) -class PortfolioOrganizationView(PortfolioBasePermissionView, View): +class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): + """ + View to handle displaying and updating the portfolio's organization details. + """ + model = Portfolio template_name = "portfolio_organization.html" - - def get(self, request, portfolio_id): - context = {} - - if self.request.user.is_authenticated: - context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") - context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature") - portfolio = get_object_or_404(Portfolio, id=portfolio_id) - context["portfolio"] = portfolio - - return render(request, "portfolio_organization.html", context) + form_class = PortfolioOrgAddressForm + context_object_name = "portfolio" + + def get_context_data(self, **kwargs): + """Add additional context data to the template.""" + context = super().get_context_data(**kwargs) + # no need to add portfolio to request context here + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + context["has_organization_feature_flag"] = flag_is_active(self.request, "organization_feature") + return context + + def get_object(self, queryset=None): + """Get the portfolio object based on the URL parameter.""" + return get_object_or_404(Portfolio, id=self.kwargs.get("portfolio_id")) + + def get_form_kwargs(self): + """Include the instance in the form kwargs.""" + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object() + return kwargs + + def get(self, request, *args, **kwargs): + """Handle GET requests to display the form.""" + self.object = self.get_object() + form = self.get_form() + return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + """Handle POST requests to process form submission.""" + 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): + """Handle the case when the form is valid.""" + self.object = form.save(commit=False) + self.object.creator = self.request.user + self.object.save() + messages.success(self.request, "The organization information for this portfolio has been updated.") + return super().form_valid(form) + + def form_invalid(self, form): + """Handle the case when the form is invalid.""" + return self.render_to_response(self.get_context_data(form=form)) + + def get_success_url(self): + """Redirect to the overview page for the portfolio.""" + return reverse("portfolio-organization", kwargs={"portfolio_id": self.object.pk})