Merge branch 'main' into ik/domain-overview

This commit is contained in:
Neil Martinsen-Burrell 2023-06-01 11:48:08 -05:00
commit d081df21ab
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
324 changed files with 236 additions and 19235 deletions

View file

@ -83,6 +83,11 @@ urlpatterns = [
views.DomainNameserversView.as_view(),
name="domain-nameservers",
),
path(
"domain/<int:pk>/your-contact-information",
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/security-email",
views.DomainSecurityEmailView.as_view(),

View file

@ -1,2 +1,7 @@
from .application_wizard import *
from .domain import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
from .domain import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)

View file

@ -3,6 +3,10 @@
from django import forms
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact
class DomainAddUserForm(forms.Form):
@ -29,3 +33,34 @@ 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."""
class Meta:
model = Contact
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
widgets = {
"first_name": forms.TextInput,
"middle_name": forms.TextInput,
"last_name": forms.TextInput,
"title": forms.TextInput,
"email": forms.EmailInput,
"phone": RegionalPhoneNumberWidget,
}
# 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", "phone"]
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
self.fields["phone"].widget.attrs.pop("maxlength", None)
for field_name in self.required:
self.fields[field_name].required = True

View file

@ -0,0 +1,44 @@
# Generated by Django 4.2.1 on 2023-05-31 23:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0022_draftdomain_domainapplication_approved_domain_and_more"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="first_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="First name",
null=True,
verbose_name="first name / given name",
),
),
migrations.AlterField(
model_name="contact",
name="last_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="Last name",
null=True,
verbose_name="last name / family name",
),
),
migrations.AlterField(
model_name="contact",
name="title",
field=models.TextField(
blank=True,
help_text="Title",
null=True,
verbose_name="title or role in your organization",
),
),
]

View file

@ -20,6 +20,7 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="First name",
verbose_name="first name / given name",
db_index=True,
)
middle_name = models.TextField(
@ -31,12 +32,14 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="Last name",
verbose_name="last name / family name",
db_index=True,
)
title = models.TextField(
null=True,
blank=True,
help_text="Title",
verbose_name="title or role in your organization",
)
email = models.TextField(
null=True,

View file

@ -562,11 +562,13 @@ class DomainApplication(TimeStampedModel):
"""Show this step if the answer to the first question implies it.
This shows for answers that aren't "Federal" or "Interstate".
This also doesnt show if user selected "School District" as well (#524)
"""
user_choice = self.organization_type
excluded = [
DomainApplication.OrganizationChoices.FEDERAL,
DomainApplication.OrganizationChoices.INTERSTATE,
DomainApplication.OrganizationChoices.SCHOOL_DISTRICT,
]
return bool(user_choice and user_choice not in excluded)

View file

@ -40,7 +40,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>

View file

@ -0,0 +1,35 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain contact information</h1>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here wont affect your Login.gov account information.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% 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 %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -13,6 +13,7 @@ import boto3_mocking # type: ignore
from registrar.models import (
DomainApplication,
Domain,
DomainInformation,
DraftDomain,
DomainInvitation,
Contact,
@ -1030,12 +1031,16 @@ class TestWithDomainPermissions(TestWithUser):
def setUp(self):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
)
def tearDown(self):
try:
self.domain_information.delete()
if hasattr(self.domain, "contacts"):
self.domain.contacts.all().delete()
self.domain.delete()
@ -1048,55 +1053,39 @@ class TestWithDomainPermissions(TestWithUser):
class TestDomainPermissions(TestWithDomainPermissions):
def test_not_logged_in(self):
"""Not logged in gets a redirect to Login."""
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user)
self.role.delete() # user no longer has a role on this domain
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
with less_console_noise():
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
@ -1312,6 +1301,23 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain contact information")
def test_domain_your_contact_information_content(self):
"""Your contact information appears on the page."""
self.domain_information.submitter = Contact(first_name="Testy")
self.domain_information.submitter.save()
self.domain_information.save()
page = self.app.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Testy")
def test_domain_security_email(self):
"""Can load domain's security email page."""
page = self.client.get(

View file

@ -2,6 +2,7 @@ from .application import *
from .domain import (
DomainView,
DomainNameserversView,
DomainYourContactInformationView,
DomainSecurityEmailView,
DomainUsersView,
DomainAddUserView,

View file

@ -14,9 +14,18 @@ from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic.edit import FormMixin
from registrar.models import DomainInvitation, User, UserDomainRole
from registrar.models import (
DomainInvitation,
User,
UserDomainRole,
)
from ..forms import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
from ..forms import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
@ -44,7 +53,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
return [{"server": server} for server in domain.nameservers()]
def get_success_url(self):
"""Redirect to the overview page for the domain."""
"""Redirect to the nameservers page for the domain."""
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
@ -96,6 +105,46 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
return super().form_valid(formset)
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
form_class = ContactForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.submitter instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.get_object().domain_info.submitter
return form_kwargs
def get_success_url(self):
"""Redirect to the your contact information for the domain."""
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
# there is a valid email address in the form
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, call setter in model."""
# Post to DB using values from the form
form.save()
messages.success(
self.request, "Your contact information for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""Domain security email editing view."""
@ -111,11 +160,11 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
return initial
def get_success_url(self):
"""Redirect to the overview page for the domain."""
"""Redirect to the security email page for the domain."""
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():

View file

@ -52,6 +52,7 @@
10038 OUTOFSCOPE http://app:8080/users
10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/your-contact-information
10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw