diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 90137c4af..93c92f1ca 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -74,6 +74,11 @@ urlpatterns = [ views.PortfolioOrganizationView.as_view(), name="organization", ), + path( + "senior-official/", + views.PortfolioSeniorOfficialView.as_view(), + name="senior-official", + ), path( "admin/logout/", RedirectView.as_view(pattern_name="logout", permanent=False), diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py index 9362c7bbd..88ec8e3f7 100644 --- a/src/registrar/forms/portfolio.py +++ b/src/registrar/forms/portfolio.py @@ -4,7 +4,7 @@ import logging from django import forms from django.core.validators import RegexValidator -from ..models import DomainInformation, Portfolio +from ..models import DomainInformation, Portfolio, SeniorOfficial logger = logging.getLogger(__name__) @@ -67,3 +67,47 @@ class PortfolioOrgAddressForm(forms.ModelForm): self.fields[field_name].required = True self.fields["state_territory"].widget.attrs.pop("maxlength", None) self.fields["zipcode"].widget.attrs.pop("maxlength", None) + + +class PortfolioSeniorOfficialForm(forms.ModelForm): + """Form for updating the portfolio senior official.""" + + JOIN = "senior_official" + + class Meta: + model = SeniorOfficial + # TODO - add full name + fields = [ + "first_name", + "last_name", + "title", + "email", + ] + + # 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. + "first_name": forms.TextInput, + "last_name": forms.TextInput, + "title": forms.TextInput, + "email": 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 = ["first_name", "last_name", "title", "email"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field_name in self.required: + self.fields[field_name].required = True diff --git a/src/registrar/templates/portfolio_organization_sidebar.html b/src/registrar/templates/portfolio_organization_sidebar.html index cfcdff3a8..cfbb30e91 100644 --- a/src/registrar/templates/portfolio_organization_sidebar.html +++ b/src/registrar/templates/portfolio_organization_sidebar.html @@ -13,7 +13,9 @@
  • - Senior official diff --git a/src/registrar/templates/portfolio_senior_official.html b/src/registrar/templates/portfolio_senior_official.html new file mode 100644 index 000000000..e22237ac4 --- /dev/null +++ b/src/registrar/templates/portfolio_senior_official.html @@ -0,0 +1,56 @@ +{% extends 'portfolio_base.html' %} +{% load static field_helpers%} + +{% block title %}Senior Official | {{ portfolio.name }} | {% endblock %} + +{% load static %} + +{% block portfolio_content %} +
    +
    +

    + Portfolio name: {{ portfolio }} +

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

    Senior Official

    + +

    Your senior official is a person within your organization who can authorize domain requests.

    + +

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

    + + {% if has_edit_org_portfolio_permission %} + {% include "includes/form_errors.html" with form=form %} + {% include "includes/required_fields.html" %} +
    + {% csrf_token %} + {% input_with_errors form.first_name %} + {% input_with_errors form.last_name %} + {% input_with_errors form.title %} + {% input_with_errors form.email %} + +
    + {% else %} +

    Full name

    +

    + {{ portfolio.senior_official.get_formatted_name }} +

    + {% if form.city.value is not None %} + {% include "includes/input_read_only.html" with field=form.title %} + {% endif %} + {% if form.state_territory.value is not None %} + {% include "includes/input_read_only.html" with field=form.email %} + {% endif %} + {% endif %} + +
    +
    +{% endblock %} diff --git a/src/registrar/views/portfolios.py b/src/registrar/views/portfolios.py index 63ebbaa01..0903b2e58 100644 --- a/src/registrar/views/portfolios.py +++ b/src/registrar/views/portfolios.py @@ -3,7 +3,7 @@ from django.http import Http404 from django.shortcuts import render from django.urls import reverse from django.contrib import messages -from registrar.forms.portfolio import PortfolioOrgAddressForm +from registrar.forms.portfolio import PortfolioOrgAddressForm, PortfolioSeniorOfficialForm from registrar.models.portfolio import Portfolio from registrar.views.utility.permission_views import ( PortfolioDomainRequestsPermissionView, @@ -94,3 +94,65 @@ class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin): def get_success_url(self): """Redirect to the overview page for the portfolio.""" return reverse("organization") + + +class PortfolioSeniorOfficialView(PortfolioBasePermissionView, FormMixin): + """ + View to handle displaying and updating the portfolio's senior official details. + """ + + model = Portfolio + template_name = "portfolio_senior_official.html" + form_class = PortfolioSeniorOfficialForm + context_object_name = "portfolio" + + def get_context_data(self, **kwargs): + """Add additional context data to the template.""" + context = super().get_context_data(**kwargs) + context["has_edit_org_portfolio_permission"] = self.request.user.has_edit_org_portfolio_permission() + return context + + def get_object(self, queryset=None): + """Get the portfolio object based on the request user.""" + portfolio = self.request.user.portfolio + if portfolio is None: + raise Http404("No organization found for this user") + return portfolio + + def get_form_kwargs(self): + """Include the instance in the form kwargs.""" + kwargs = super().get_form_kwargs() + kwargs["instance"] = self.get_object().senior_official + 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.""" + senior_official = form.save() + portfolio = self.get_object() + portfolio.senior_official = senior_official + portfolio.save() + messages.success(self.request, "The Senior Official 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("senior-official") \ No newline at end of file