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" %}
+
+
+
+
{% 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})