mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-24 03:30:50 +02:00
Merge pull request #648 from cisagov/rjm/520-contact-information
Form for editing your contact information
This commit is contained in:
commit
a6e06ae183
11 changed files with 234 additions and 50 deletions
|
@ -83,6 +83,11 @@ urlpatterns = [
|
||||||
views.DomainNameserversView.as_view(),
|
views.DomainNameserversView.as_view(),
|
||||||
name="domain-nameservers",
|
name="domain-nameservers",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"domain/<int:pk>/your-contact-information",
|
||||||
|
views.DomainYourContactInformationView.as_view(),
|
||||||
|
name="domain-your-contact-information",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"domain/<int:pk>/security-email",
|
"domain/<int:pk>/security-email",
|
||||||
views.DomainSecurityEmailView.as_view(),
|
views.DomainSecurityEmailView.as_view(),
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
from .application_wizard import *
|
from .application_wizard import *
|
||||||
from .domain import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm
|
from .domain import (
|
||||||
|
DomainAddUserForm,
|
||||||
|
NameserverFormset,
|
||||||
|
DomainSecurityEmailForm,
|
||||||
|
ContactForm,
|
||||||
|
)
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
|
||||||
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
|
||||||
|
from ..models import Contact
|
||||||
|
|
||||||
|
|
||||||
class DomainAddUserForm(forms.Form):
|
class DomainAddUserForm(forms.Form):
|
||||||
|
|
||||||
|
@ -29,3 +33,34 @@ class DomainSecurityEmailForm(forms.Form):
|
||||||
"""Form for adding or editing a security email to a domain."""
|
"""Form for adding or editing a security email to a domain."""
|
||||||
|
|
||||||
security_email = forms.EmailField(label="Security email")
|
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
|
||||||
|
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,6 +20,7 @@ class Contact(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="First name",
|
help_text="First name",
|
||||||
|
verbose_name="first name / given name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
middle_name = models.TextField(
|
middle_name = models.TextField(
|
||||||
|
@ -31,12 +32,14 @@ class Contact(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Last name",
|
help_text="Last name",
|
||||||
|
verbose_name="last name / family name",
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
title = models.TextField(
|
title = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Title",
|
help_text="Title",
|
||||||
|
verbose_name="title or role in your organization",
|
||||||
)
|
)
|
||||||
email = models.TextField(
|
email = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="usa-sidenav__item">
|
<li class="usa-sidenav__item">
|
||||||
{% url 'todo' as url %}
|
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||||
<a href="{{ url }}"
|
<a href="{{ url }}"
|
||||||
{% if request.path == url %}class="usa-current"{% endif %}
|
{% if request.path == url %}class="usa-current"{% endif %}
|
||||||
>
|
>
|
||||||
|
|
35
src/registrar/templates/domain_your_contact_information.html
Normal file
35
src/registrar/templates/domain_your_contact_information.html
Normal 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 you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t 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 #}
|
|
@ -13,6 +13,7 @@ import boto3_mocking # type: ignore
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainApplication,
|
DomainApplication,
|
||||||
Domain,
|
Domain,
|
||||||
|
DomainInformation,
|
||||||
DraftDomain,
|
DraftDomain,
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
Contact,
|
Contact,
|
||||||
|
@ -1030,12 +1031,16 @@ class TestWithDomainPermissions(TestWithUser):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
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(
|
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
try:
|
try:
|
||||||
|
self.domain_information.delete()
|
||||||
if hasattr(self.domain, "contacts"):
|
if hasattr(self.domain, "contacts"):
|
||||||
self.domain.contacts.all().delete()
|
self.domain.contacts.all().delete()
|
||||||
self.domain.delete()
|
self.domain.delete()
|
||||||
|
@ -1048,55 +1053,39 @@ class TestWithDomainPermissions(TestWithUser):
|
||||||
class TestDomainPermissions(TestWithDomainPermissions):
|
class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
def test_not_logged_in(self):
|
def test_not_logged_in(self):
|
||||||
"""Not logged in gets a redirect to Login."""
|
"""Not logged in gets a redirect to Login."""
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
for view_name in [
|
||||||
self.assertEqual(response.status_code, 302)
|
"domain",
|
||||||
|
"domain-users",
|
||||||
response = self.client.get(
|
"domain-users-add",
|
||||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
"domain-nameservers",
|
||||||
)
|
"domain-your-contact-information",
|
||||||
self.assertEqual(response.status_code, 302)
|
"domain-security-email",
|
||||||
|
]:
|
||||||
response = self.client.get(
|
with self.subTest(view_name=view_name):
|
||||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
response = self.client.get(
|
||||||
)
|
reverse(view_name, kwargs={"pk": self.domain.id})
|
||||||
self.assertEqual(response.status_code, 302)
|
)
|
||||||
|
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)
|
|
||||||
|
|
||||||
def test_no_domain_role(self):
|
def test_no_domain_role(self):
|
||||||
"""Logged in but no role gets 403 Forbidden."""
|
"""Logged in but no role gets 403 Forbidden."""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
self.role.delete() # user no longer has a role on this domain
|
self.role.delete() # user no longer has a role on this domain
|
||||||
|
|
||||||
with less_console_noise():
|
for view_name in [
|
||||||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
"domain",
|
||||||
self.assertEqual(response.status_code, 403)
|
"domain-users",
|
||||||
|
"domain-users-add",
|
||||||
with less_console_noise():
|
"domain-nameservers",
|
||||||
response = self.client.get(
|
"domain-your-contact-information",
|
||||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
"domain-security-email",
|
||||||
)
|
]:
|
||||||
self.assertEqual(response.status_code, 403)
|
with self.subTest(view_name=view_name):
|
||||||
|
with less_console_noise():
|
||||||
with less_console_noise():
|
response = self.client.get(
|
||||||
response = self.client.get(
|
reverse(view_name, kwargs={"pk": self.domain.id})
|
||||||
reverse("domain-users-add", kwargs={"pk": self.domain.id})
|
)
|
||||||
)
|
self.assertEqual(response.status_code, 403)
|
||||||
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)
|
|
||||||
|
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
@ -1312,6 +1301,23 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
||||||
# the field.
|
# the field.
|
||||||
self.assertContains(result, "This field is required", count=2, status_code=200)
|
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):
|
def test_domain_security_email(self):
|
||||||
"""Can load domain's security email page."""
|
"""Can load domain's security email page."""
|
||||||
page = self.client.get(
|
page = self.client.get(
|
||||||
|
|
|
@ -2,6 +2,7 @@ from .application import *
|
||||||
from .domain import (
|
from .domain import (
|
||||||
DomainView,
|
DomainView,
|
||||||
DomainNameserversView,
|
DomainNameserversView,
|
||||||
|
DomainYourContactInformationView,
|
||||||
DomainSecurityEmailView,
|
DomainSecurityEmailView,
|
||||||
DomainUsersView,
|
DomainUsersView,
|
||||||
DomainAddUserView,
|
DomainAddUserView,
|
||||||
|
|
|
@ -14,9 +14,18 @@ from django.shortcuts import redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic.edit import FormMixin
|
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.email import send_templated_email, EmailSendingError
|
||||||
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
||||||
|
|
||||||
|
@ -44,7 +53,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||||
return [{"server": server} for server in domain.nameservers()]
|
return [{"server": server} for server in domain.nameservers()]
|
||||||
|
|
||||||
def get_success_url(self):
|
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})
|
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -96,6 +105,46 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||||
return super().form_valid(formset)
|
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):
|
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||||
|
|
||||||
"""Domain security email editing view."""
|
"""Domain security email editing view."""
|
||||||
|
@ -111,11 +160,11 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def get_success_url(self):
|
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})
|
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Formset submission posts to this view."""
|
"""Form submission posts to this view."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
form = self.get_form()
|
form = self.get_form()
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/users
|
10038 OUTOFSCOPE http://app:8080/users
|
||||||
10038 OUTOFSCOPE http://app:8080/users/add
|
10038 OUTOFSCOPE http://app:8080/users/add
|
||||||
10038 OUTOFSCOPE http://app:8080/nameservers
|
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/security-email
|
||||||
10038 OUTOFSCOPE http://app:8080/delete
|
10038 OUTOFSCOPE http://app:8080/delete
|
||||||
10038 OUTOFSCOPE http://app:8080/withdraw
|
10038 OUTOFSCOPE http://app:8080/withdraw
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue