diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index a63e10fe1..a19da4791 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//authorizing-official", + views.DomainAuthorizingOfficialView.as_view(), + name="domain-authorizing-official", + ), path( "domain//security-email", views.DomainSecurityEmailView.as_view(), diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 3668c3018..e6fbc8fee 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -28,13 +28,6 @@ NameserverFormset = formset_factory( ) -class DomainSecurityEmailForm(forms.Form): - - """Form for adding or editing a security email to a domain.""" - - security_email = forms.EmailField(label="Security email") - - class ContactForm(forms.ModelForm): """Form for updating contacts.""" @@ -59,8 +52,15 @@ class ContactForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # take off maxlength attribute for the phone number field - # which interferes with our input_with_errors template tag + # which interferes with out input_with_errors template tag self.fields["phone"].widget.attrs.pop("maxlength", None) for field_name in self.required: self.fields[field_name].required = True + + +class DomainSecurityEmailForm(forms.Form): + + """Form for adding or editing a security email to a domain.""" + + security_email = forms.EmailField(label="Security email") diff --git a/src/registrar/migrations/0024_alter_contact_email.py b/src/registrar/migrations/0024_alter_contact_email.py new file mode 100644 index 000000000..f512d5d82 --- /dev/null +++ b/src/registrar/migrations/0024_alter_contact_email.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-06-01 19:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0023_alter_contact_first_name_alter_contact_last_name_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="contact", + name="email", + field=models.EmailField( + blank=True, db_index=True, help_text="Email", max_length=254, null=True + ), + ), + ] diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index cbfde7a23..41ed9f2c5 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -41,7 +41,7 @@ class Contact(TimeStampedModel): help_text="Title", verbose_name="title or role in your organization", ) - email = models.TextField( + email = models.EmailField( null=True, blank=True, help_text="Email", diff --git a/src/registrar/templates/domain_authorizing_official.html b/src/registrar/templates/domain_authorizing_official.html new file mode 100644 index 000000000..445db5707 --- /dev/null +++ b/src/registrar/templates/domain_authorizing_official.html @@ -0,0 +1,43 @@ +{% extends "domain_base.html" %} +{% load static field_helpers%} + +{% block title %}Domain authorizing official | {{ 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 %} + +

Authorizing official

+ +

Your authorizing official is the person within your organization who can + authorize domain requests. This is generally the highest-ranking or + highest-elected official in your organization. Read more about who can serve + as an authorizing official.

+ + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + + {% input_with_errors form.first_name %} + + {% input_with_errors form.middle_name %} + + {% input_with_errors form.last_name %} + + {% input_with_errors form.title %} + + {% input_with_errors form.email %} + + {% input_with_errors form.phone %} + + + + +
+ +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 8ba72547d..5b0457e55 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -31,7 +31,7 @@
  • - {% url 'todo' as url %} + {% url 'domain-authorizing-official' pk=domain.id as url %} diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index 94f985fab..173362943 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -15,6 +15,7 @@ from registrar.forms.application_wizard import ( AnythingElseForm, TypeOfWorkForm, ) +from registrar.forms.domain import ContactForm class TestFormValidation(TestCase): @@ -277,3 +278,13 @@ class TestFormValidation(TestCase): for error in form.non_field_errors() ) ) + + +class TestContactForm(TestCase): + def test_contact_form_email_invalid(self): + form = ContactForm(data={"email": "example.net"}) + self.assertEqual(form.errors["email"], ["Enter a valid email address."]) + + def test_contact_form_email_invalid2(self): + form = ContactForm(data={"email": "@"}) + self.assertEqual(form.errors["email"], ["Enter a valid email address."]) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index a836f87e8..bf1cef2f2 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1058,6 +1058,7 @@ class TestDomainPermissions(TestWithDomainPermissions): "domain-users", "domain-users-add", "domain-nameservers", + "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-authorizing-official", "domain-your-contact-information", "domain-security-email", ]: @@ -1087,12 +1089,6 @@ class TestDomainPermissions(TestWithDomainPermissions): ) self.assertEqual(response.status_code, 403) - with less_console_noise(): - response = self.client.get( - reverse("domain-security-email", kwargs={"pk": self.domain.id}) - ) - self.assertEqual(response.status_code, 403) - class TestDomainDetail(TestWithDomainPermissions, WebTest): def setUp(self): @@ -1301,6 +1297,24 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): # the field. self.assertContains(result, "This field is required", count=2, status_code=200) + def test_domain_authorizing_official(self): + """Can load domain's authorizing official page.""" + page = self.client.get( + reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}) + ) + # once on the sidebar, once in the title + self.assertContains(page, "Authorizing official", count=2) + + def test_domain_authorizing_official_content(self): + """Authorizing official information appears on the page.""" + self.domain_information.authorizing_official = Contact(first_name="Testy") + self.domain_information.authorizing_official.save() + self.domain_information.save() + page = self.app.get( + reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}) + ) + self.assertContains(page, "Testy") + 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 758d04d72..8212d140d 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -1,6 +1,7 @@ from .application import * from .domain import ( DomainView, + DomainAuthorizingOfficialView, DomainNameserversView, DomainYourContactInformationView, DomainSecurityEmailView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index cc3f79809..28e053470 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -15,6 +15,7 @@ from django.urls import reverse from django.views.generic.edit import FormMixin from registrar.models import ( + Domain, DomainInvitation, User, UserDomainRole, @@ -40,6 +41,48 @@ class DomainView(DomainPermissionView): template_name = "domain_detail.html" +class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin): + + """Domain authorizing official editing view.""" + + model = Domain + template_name = "domain_authorizing_official.html" + context_object_name = "domain" + form_class = ContactForm + + def get_form_kwargs(self, *args, **kwargs): + """Add domain_info.authorizing_official instance to make a bound form.""" + form_kwargs = super().get_form_kwargs(*args, **kwargs) + form_kwargs["instance"] = self.get_object().domain_info.authorizing_official + return form_kwargs + + def get_success_url(self): + """Redirect to the overview page for the domain.""" + return reverse("domain-authorizing-official", 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 authorizing official.""" + form.save() + + messages.success( + self.request, "The authorizing official for this domain has been updated." + ) + # superclass has the redirect + return super().form_valid(form) + + class DomainNameserversView(DomainPermissionView, FormMixin): """Domain nameserver editing view."""