mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-15 05:54:11 +02:00
Merge branch 'main' into litterbox/2399-fill-senior-official
This commit is contained in:
commit
f1153a9c30
8 changed files with 283 additions and 17 deletions
|
@ -10,3 +10,6 @@ from .domain import (
|
||||||
DomainDsdataFormset,
|
DomainDsdataFormset,
|
||||||
DomainDsdataForm,
|
DomainDsdataForm,
|
||||||
)
|
)
|
||||||
|
from .portfolio import (
|
||||||
|
PortfolioOrgAddressForm,
|
||||||
|
)
|
||||||
|
|
|
@ -458,7 +458,7 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
# the database fields have blank=True so ModelForm doesn't create
|
# the database fields have blank=True so ModelForm doesn't create
|
||||||
# required fields by default. Use this list in __init__ to mark each
|
# required fields by default. Use this list in __init__ to mark each
|
||||||
# of these fields as required
|
# 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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
69
src/registrar/forms/portfolio.py
Normal file
69
src/registrar/forms/portfolio.py
Normal file
|
@ -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)
|
|
@ -6,11 +6,6 @@ from registrar.models.federal_agency import FederalAgency
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
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):
|
class Portfolio(TimeStampedModel):
|
||||||
"""
|
"""
|
||||||
Portfolio is used for organizing domains/domain-requests into
|
Portfolio is used for organizing domains/domain-requests into
|
||||||
|
|
|
@ -1,8 +1,64 @@
|
||||||
{% extends 'portfolio_base.html' %}
|
{% extends 'portfolio_base.html' %}
|
||||||
|
{% load static field_helpers%}
|
||||||
|
|
||||||
|
{% block title %}Organization mailing address | {{ portfolio.name }} | {% endblock %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block portfolio_content %}
|
{% block portfolio_content %}
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div class="tablet:grid-col-3">
|
||||||
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
|
text-primary-darker text-semibold"
|
||||||
|
>
|
||||||
|
<span class="usa-sr-only"> Portfolio name:</span> {{ portfolio }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% include 'portfolio_organization_sidebar.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tablet:grid-col-9">
|
||||||
|
|
||||||
<h1>Organization</h1>
|
<h1>Organization</h1>
|
||||||
|
|
||||||
|
<p>The name of your federal agency will be publicly listed as the domain registrant.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The federal agency for your organization can’t be updated here.
|
||||||
|
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
|
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong class="text-primary display-block margin-bottom-1">Federal agency</strong>
|
||||||
|
{{ portfolio }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
23
src/registrar/templates/portfolio_organization_sidebar.html
Normal file
23
src/registrar/templates/portfolio_organization_sidebar.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||||
|
<nav aria-label="Domain sections">
|
||||||
|
<ul class="usa-sidenav">
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
{% url 'portfolio-organization' portfolio_id=portfolio.id as url %}
|
||||||
|
<a href="{{ url }}"
|
||||||
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
|
>
|
||||||
|
Organization
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="usa-sidenav__item">
|
||||||
|
<a href="#"
|
||||||
|
>
|
||||||
|
Senior official
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
|
@ -1,5 +1,6 @@
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
from registrar.config import settings
|
||||||
from registrar.models.portfolio import Portfolio
|
from registrar.models.portfolio import Portfolio
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
|
@ -17,7 +18,7 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TestPortfolioViews(WebTest):
|
class TestPortfolio(WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = create_test_user()
|
self.user = create_test_user()
|
||||||
|
@ -192,3 +193,70 @@ class TestPortfolioViews(WebTest):
|
||||||
self.assertNotContains(
|
self.assertNotContains(
|
||||||
portfolio_page, reverse("portfolio-domain-requests", kwargs={"portfolio_id": self.portfolio.pk})
|
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")
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import logging
|
||||||
from django.shortcuts import get_object_or_404, render
|
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.models.portfolio import Portfolio
|
||||||
from registrar.views.utility.permission_views import (
|
from registrar.views.utility.permission_views import (
|
||||||
PortfolioDomainRequestsPermissionView,
|
PortfolioDomainRequestsPermissionView,
|
||||||
|
@ -7,6 +11,10 @@ from registrar.views.utility.permission_views import (
|
||||||
)
|
)
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from django.views.generic.edit import FormMixin
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
|
||||||
|
@ -42,17 +50,61 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
||||||
return render(request, "portfolio_requests.html", context)
|
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"
|
template_name = "portfolio_organization.html"
|
||||||
|
form_class = PortfolioOrgAddressForm
|
||||||
|
context_object_name = "portfolio"
|
||||||
|
|
||||||
def get(self, request, portfolio_id):
|
def get_context_data(self, **kwargs):
|
||||||
context = {}
|
"""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
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
def get_object(self, queryset=None):
|
||||||
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
"""Get the portfolio object based on the URL parameter."""
|
||||||
context["has_organization_feature_flag"] = flag_is_active(request, "organization_feature")
|
return get_object_or_404(Portfolio, id=self.kwargs.get("portfolio_id"))
|
||||||
portfolio = get_object_or_404(Portfolio, id=portfolio_id)
|
|
||||||
context["portfolio"] = portfolio
|
|
||||||
|
|
||||||
return render(request, "portfolio_organization.html", context)
|
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})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue