Merge branch 'main' into nmb/rbac

This commit is contained in:
Neil Martinsen-Burrell 2023-03-10 10:07:00 -06:00
commit 2451cd8d11
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
29 changed files with 512 additions and 270 deletions

View file

@ -4,7 +4,7 @@ Date: 2022-09-26
## Status
Proposed
Superseded by [20. User models revisited, plus WHOIS](./0020-user-models-revisited-plus-whois.md)
## Context

View file

@ -0,0 +1,103 @@
# 20. User models revisited, plus WHOIS
Date: 2022-03-01
## Status
Accepted
## Context
In the process of thinking through the details of registry implementation and role-based access control, it has become clear that the registrar has 3 types of contacts:
1. Those for the purpose of allowing CISA to verify the authenticity of a request. In other words, is the organization an eligible U.S.-based government and is the requestor duly authorized to make the request on behalf of their government?
1. Those for the purpose of managing a domain or its DNS configuration.
* There is ambiguous overlap remaining between use case 1 and 2.
1. Those for the purpose of publishing publicly in WHOIS.
Additionally, there are two mental models of contacts that impact the permissions associated with them and how they can be updated:
1. A contact represents a person: changes made in one part of the system will update in all parts of the system; people are not allowed to make updates unless they are authorized.
1. A contact represents information filled out on a sheet of paper: changes on one “copy” of the information will not update other “copies” of the information; people are allowed to make updates based on their authorization to access and edit the “sheet of paper”.
## Decision
To have a custom `User` model containing un-editable data derived from Login.gov and updated automatically each time a user logs in. In role-based access control, User is the model to which roles attach.
To have a `Contact` model which stores name and contact data. The presence of a foreign key from Contact to User indicates that that contact data has been associated with a Login.gov user account. If a User is deleted, the foreign key column is set to null.
User and Contact follow the “person” mental model.
To have a `PublicContact` model which stores WHOIS data. Domains will be created with the following default values.
PublicContact follows the “sheet of paper” mental model.
### Registrant default values
| Field | Value |
|---|---|
|name | CSD/CB Attn: Cameron Dixon
|org | Cybersecurity and Infrastructure Security Agency
|street1 | CISA NGR STOP 0645
|street2 | 1110 N. Glebe Rd.
|city | Arlington
|sp | VA
|pc | 20598-0645
|cc | US
### Administrative default values
| Field | Value |
|---|---|
|name | Program Manager
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | dotgov@cisa.dhs.gov
### Technical default values
Whether this contact will be created by default or not is yet to be determined.
| Field | Value |
|---|---|
|name | Registry Customer Service
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | registrar@dotgov.gov
### Security default values
Whether this contact will be created by default or not is yet to be determined.
The EPP “disclose tags” feature might be used to publish only the email address.
| Field | Value |
|---|---|
|name | Registry Customer Service
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | registrar@dotgov.gov
## Consequences
This has minimal impact on the code weve developed so far.
By having PublicContact be an entirely separate model, it ensures that (for better or worse) WHOIS contact data must be updated separately from general Contacts. At present, CISA intends to allow registrants to edit only one contact: security, so this is a minor point of low impact.
In a future state where CISA allows more to be published, it is easy to imagine a set of checkboxes on a contact update form: “[ ] publish this as my technical contact for example.gov”, etc.

View file

@ -28,18 +28,14 @@ class OpenIdConnectBackend(ModelBackend):
UserModel = get_user_model()
username = self.clean_username(kwargs["sub"])
if "upn" in kwargs.keys():
username = kwargs["upn"]
# Some OP may actually choose to withhold some information, so we must
# test if it is present
openid_data = {"last_login": timezone.now()}
openid_data["first_name"] = kwargs.get("first_name", "")
openid_data["first_name"] = kwargs.get("given_name", "")
openid_data["first_name"] = kwargs.get("christian_name", "")
openid_data["last_name"] = kwargs.get("family_name", "")
openid_data["last_name"] = kwargs.get("last_name", "")
openid_data["email"] = kwargs.get("email", "")
openid_data["phone"] = kwargs.get("phone", "")
# Note that this could be accomplished in one try-except clause, but
# instead we use get_or_create when creating unknown users since it has
@ -47,6 +43,7 @@ class OpenIdConnectBackend(ModelBackend):
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
args = {
UserModel.USERNAME_FIELD: username,
# defaults _will_ be updated, these are not fallbacks
"defaults": openid_data,
}
user, created = UserModel.objects.update_or_create(**args)
@ -56,10 +53,7 @@ class OpenIdConnectBackend(ModelBackend):
try:
user = UserModel.objects.get_by_natural_key(username)
except UserModel.DoesNotExist:
try:
user = UserModel.objects.get(email=kwargs["email"])
except UserModel.DoesNotExist:
return None
return None
return user
def clean_username(self, username):

View file

@ -22,18 +22,18 @@ class AuditedAdmin(admin.ModelAdmin):
)
class UserProfileInline(admin.StackedInline):
class UserContactInline(admin.StackedInline):
"""Edit a user's profile on the user page."""
model = models.UserProfile
model = models.Contact
class MyUserAdmin(UserAdmin):
"""Custom user admin class to use our inlines."""
inlines = [UserProfileInline]
inlines = [UserContactInline]
class HostIPInline(admin.StackedInline):

View file

@ -645,7 +645,6 @@ if DEBUG:
NPLUSONE_RAISE = False
NPLUSONE_WHITELIST = [
{"model": "admin.LogEntry", "field": "user"},
{"model": "registrar.UserProfile"},
]
# insert the amazing django-debug-toolbar

View file

@ -53,7 +53,6 @@ urlpatterns = [
name=views.ApplicationWizard.EDIT_URL_NAME,
),
path("health/", views.health),
path("edit_profile/", views.edit_profile, name="edit-profile"),
path("openid/", include("djangooidc.urls")),
path("register/", include((application_urls, APPLICATION_NAMESPACE))),
path("api/v1/available/<domain>", available, name="available"),

View file

@ -4,7 +4,6 @@ from faker import Faker
from registrar.models import (
User,
UserProfile,
DomainApplication,
Domain,
Contact,
@ -57,8 +56,6 @@ class UserFixture:
user.is_active = True
user.save()
logger.debug("User object created for %s" % admin["first_name"])
UserProfile.objects.get_or_create(user=user)
logger.debug("Profile object created for %s" % admin["first_name"])
except Exception as e:
logger.warning(e)
logger.debug("All users loaded.")

View file

@ -1,2 +1 @@
from .edit_profile import *
from .application_wizard import *

View file

@ -226,8 +226,10 @@ class OrganizationElectionForm(RegistrarForm):
is_election_board = self.cleaned_data["is_election_board"]
if is_election_board is None:
raise forms.ValidationError(
"Select “Yes” if you represent an election office. Select “No” if you"
" dont.",
(
"Select “Yes” if you represent an election office. Select “No” if"
" you dont."
),
code="required",
)
return is_election_board
@ -399,6 +401,12 @@ class CurrentSitesForm(RegistrarForm):
website = forms.URLField(
required=False,
label="Public website",
error_messages={
"invalid": (
"Enter your organization's website in the required format, like"
" www.city.com."
)
},
)
@ -676,7 +684,7 @@ class NoOtherContactsForm(RegistrarForm):
# label has to end in a space to get the label_suffix to show
label=(
"Please explain why there are no other employees from your organization"
" that we can contact."
" we can contact to help us assess your eligibility for a .gov domain."
),
widget=forms.Textarea(),
)
@ -692,7 +700,7 @@ class AnythingElseForm(RegistrarForm):
class RequirementsForm(RegistrarForm):
is_policy_acknowledged = forms.BooleanField(
label=("I read and agree to the requirements for operating .gov domains."),
label="I read and agree to the requirements for operating .gov domains.",
error_messages={
"required": (
"Check the box if you read and agree to the requirements for"

View file

@ -1,21 +0,0 @@
from django import forms
from ..models import UserProfile
class EditProfileForm(forms.ModelForm):
"""Custom form class for editing a UserProfile.
We can add whatever fields we want to this form and customize how they
are displayed. The form is rendered into a template `profile.html` by a
view called `edit_profile` in `profile.py`.
"""
display_name = forms.CharField(
widget=forms.TextInput(attrs={"class": "usa-input"}), label="Display Name"
)
class Meta:
model = UserProfile
fields = ["display_name"]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.1.6 on 2023-03-07 14:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0011_remove_domainapplication_security_email"),
]
operations = [
migrations.DeleteModel(
name="UserProfile",
),
]

View file

@ -0,0 +1,68 @@
# Generated by Django 4.1.6 on 2023-03-07 14:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0012_delete_userprofile"),
]
operations = [
migrations.CreateModel(
name="PublicContact",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"contact_type",
models.CharField(
choices=[
("registrant", "Registrant"),
("administrative", "Administrative"),
("technical", "Technical"),
("security", "Security"),
],
max_length=14,
),
),
("name", models.TextField()),
("org", models.TextField(null=True)),
("street1", models.TextField()),
("street2", models.TextField(null=True)),
("street3", models.TextField(null=True)),
("city", models.TextField()),
("sp", models.TextField()),
("pc", models.TextField()),
("cc", models.TextField()),
("email", models.TextField()),
("voice", models.TextField()),
("fax", models.TextField(null=True)),
("pw", models.TextField()),
],
options={
"abstract": False,
},
),
migrations.AddField(
model_name="contact",
name="user",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 4.1.6 on 2023-03-07 16:39
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields # type: ignore
class Migration(migrations.Migration):
dependencies = [
("registrar", "0013_publiccontact_contact_user"),
]
operations = [
migrations.AddField(
model_name="user",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(
blank=True,
db_index=True,
help_text="Phone",
max_length=128,
null=True,
region=None,
),
),
]

View file

@ -1,4 +1,4 @@
# Generated by Django 4.1.6 on 2023-03-06 17:26
# Generated by Django 4.1.6 on 2023-03-10 15:32
from django.conf import settings
from django.db import migrations, models
@ -7,7 +7,7 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0011_remove_domainapplication_security_email"),
("registrar", "0014_user_phone_alter_contact_user"),
]
operations = [

View file

@ -7,7 +7,7 @@ from .host_ip import HostIP
from .host import Host
from .nameserver import Nameserver
from .user_domain_role import UserDomainRole
from .user_profile import UserProfile
from .public_contact import PublicContact
from .user import User
from .website import Website
@ -19,7 +19,7 @@ __all__ = [
"Host",
"Nameserver",
"UserDomainRole",
"UserProfile",
"PublicContact",
"User",
"Website",
]
@ -31,6 +31,6 @@ auditlog.register(HostIP)
auditlog.register(Host)
auditlog.register(Nameserver)
auditlog.register(UserDomainRole)
auditlog.register(UserProfile)
auditlog.register(PublicContact)
auditlog.register(User)
auditlog.register(Website)

View file

@ -9,6 +9,13 @@ class Contact(TimeStampedModel):
"""Contact information follows a similar pattern for each contact."""
user = models.OneToOneField(
"registrar.User",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
first_name = models.TextField(
null=True,
blank=True,

View file

@ -0,0 +1,50 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
class PublicContact(TimeStampedModel):
"""Contact information intended to be published in WHOIS."""
class ContactTypeChoices(models.TextChoices):
"""These are the types of contacts accepted by the registry."""
REGISTRANT = "registrant", "Registrant"
ADMINISTRATIVE = "administrative", "Administrative"
TECHNICAL = "technical", "Technical"
SECURITY = "security", "Security"
contact_type = models.CharField(max_length=14, choices=ContactTypeChoices.choices)
# contact's full name
name = models.TextField(null=False)
# contact's organization (null ok)
org = models.TextField(null=True)
# contact's street
street1 = models.TextField(null=False)
# contact's street (null ok)
street2 = models.TextField(null=True)
# contact's street (null ok)
street3 = models.TextField(null=True)
# contact's city
city = models.TextField(null=False)
# contact's state or province
sp = models.TextField(null=False)
# contact's postal code
pc = models.TextField(null=False)
# contact's country code
cc = models.TextField(null=False)
# contact's email address
email = models.TextField(null=False)
# contact's phone number
# Must be in ITU.E164.2005 format
voice = models.TextField(null=False)
# contact's fax number (null ok)
# Must be in ITU.E164.2005 format
fax = models.TextField(null=True)
# contact's authorization code
# 16 characters minium
pw = models.TextField(null=False)
def __str__(self):
return f"{self.name} <{self.email}>"

View file

@ -1,6 +1,8 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
class User(AbstractUser):
"""
@ -14,6 +16,13 @@ class User(AbstractUser):
related_name="users",
)
phone = PhoneNumberField(
null=True,
blank=True,
help_text="Phone",
db_index=True,
)
def __str__(self):
# this info is pulled from Login.gov
if self.first_name or self.last_name:

View file

@ -1,28 +0,0 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
from .utility.address_model import AddressModel
from .contact import Contact
class UserProfile(Contact, TimeStampedModel, AddressModel):
"""User information, unrelated to their login/auth details."""
user = models.OneToOneField(
"registrar.User",
null=True,
blank=True,
on_delete=models.CASCADE,
)
display_name = models.TextField()
def __str__(self):
# use info stored in User rather than Contact,
# because Contact is user-editable while User
# pulls from identity-verified Login.gov
try:
return str(self.user)
except Exception:
return "Orphaned account"

View file

@ -1,27 +0,0 @@
from django.db import models
class AddressModel(models.Model):
"""
An abstract base model that provides common fields
for postal addresses.
"""
# contact's street (null ok)
street1 = models.TextField(blank=True)
# contact's street (null ok)
street2 = models.TextField(blank=True)
# contact's street (null ok)
street3 = models.TextField(blank=True)
# contact's city
city = models.TextField(blank=True)
# contact's state or province (null ok)
sp = models.TextField(blank=True)
# contact's postal code (null ok)
pc = models.TextField(blank=True)
# contact's country code
cc = models.TextField(blank=True)
class Meta:
abstract = True
# don't put anything else here, it will be ignored

View file

@ -5,7 +5,7 @@ from django.core.management import call_command
from django.db.models.signals import post_save, post_migrate
from django.dispatch import receiver
from .models import User, UserProfile
from .models import User, Contact
logger = logging.getLogger(__name__)
@ -15,18 +15,46 @@ logger = logging.getLogger(__name__)
def handle_profile(sender, instance, **kwargs):
"""Method for when a User is saved.
If the user is being created, then create a matching UserProfile. Otherwise
save an updated profile or create one if it doesn't exist.
A first time registrant may have been invited, so we'll search for a matching
Contact record, by email address, and associate them, if possible.
A first time registrant may not have a matching Contact, so we'll create one,
copying the contact values we received from Login.gov in order to initialize it.
During subsequent login, a User record may be updated with new data from Login.gov,
but in no case will we update contact values on an existing Contact record.
"""
if kwargs.get("created", False):
UserProfile.objects.create(user=instance)
first_name = getattr(instance, "first_name", "")
last_name = getattr(instance, "last_name", "")
email = getattr(instance, "email", "")
phone = getattr(instance, "phone", "")
is_new_user = kwargs.get("created", False)
if is_new_user:
contacts = Contact.objects.filter(email=email)
else:
# the user is not being created.
if hasattr(instance, "userprofile"):
instance.userprofile.save()
else:
UserProfile.objects.create(user=instance)
contacts = Contact.objects.filter(user=instance)
if len(contacts) == 0: # no matching contact
Contact.objects.create(
user=instance,
first_name=first_name,
last_name=last_name,
email=email,
phone=phone,
)
if len(contacts) >= 1 and is_new_user: # a matching contact
contacts[0].user = instance
contacts[0].save()
if len(contacts) > 1: # multiple matches
logger.warning(
"There are multiple Contacts with the same email address."
f" Picking #{contacts[0].id} for User #{instance.id}."
)
@receiver(post_migrate)

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Thank you for your domain request{% endblock %}
{% block title %}Thanks for your domain request!{% endblock %}
{% block content %}
<main id="main-content" class="grid-container register-form-step">
@ -14,7 +14,7 @@
/>
<h1>Thank you</h1>
</span>
<p> Thank you for your domain request. We'll email a copy of your request to you,
<p>We'll email a copy of your request to you,
your authorizing official, and any contacts you added.</p>
<h2>Next steps in this process</h2>

View file

@ -70,12 +70,6 @@
class="usa-button"
>Submit your domain request</button>
{% endif %}
<button
type="submit"
name="submit_button"
value="save"
class="usa-button usa-button--outline"
>Save progress</button>
</div>
{% endblock %}

View file

@ -2,8 +2,7 @@
{% load field_helpers %}
{% block form_instructions %}
<p> Well use the following information to contact you about your domain request and,
once your request is approved, about managing your domain.</p>
<p>Well use this information to contact you about your domain request.</p>
<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
@ -35,4 +34,4 @@
{% endwith %}
</fieldset>
{% endblock %}
{% endblock %}

View file

@ -29,7 +29,13 @@ class TestFormValidation(TestCase):
def test_website_invalid(self):
form = CurrentSitesForm(data={"website": "nah"})
self.assertEqual(form.errors["website"], ["Enter a valid URL."])
self.assertEqual(
form.errors["website"],
[
"Enter your organization's"
" website in the required format, like www.city.com."
],
)
def test_website_valid(self):
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
@ -83,7 +89,7 @@ class TestFormValidation(TestCase):
"""Must be a valid phone number."""
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})
self.assertTrue(
form.errors["phone"][0].startswith("Enter a valid phone number")
form.errors["phone"][0].startswith("Enter a valid phone number ")
)
def test_your_contact_email_invalid(self):
@ -98,7 +104,7 @@ class TestFormValidation(TestCase):
"""Must be a valid phone number."""
form = YourContactForm(data={"phone": "boss@boss"})
self.assertTrue(
form.errors["phone"][0].startswith("Enter a valid phone number")
form.errors["phone"][0].startswith("Enter a valid phone number ")
)
def test_other_contact_email_invalid(self):
@ -113,7 +119,7 @@ class TestFormValidation(TestCase):
"""Must be a valid phone number."""
form = OtherContactsForm(data={"phone": "boss@boss"})
self.assertTrue(
form.errors["phone"][0].startswith("Enter a valid phone number")
form.errors["phone"][0].startswith("Enter a valid phone number ")
)
def test_requirements_form_blank(self):

View file

@ -0,0 +1,103 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from registrar.models import Contact
class TestUserPostSave(TestCase):
def setUp(self):
self.username = "test_user"
self.first_name = "First"
self.last_name = "Last"
self.email = "info@example.com"
self.phone = "202-555-0133"
self.preferred_first_name = "One"
self.preferred_last_name = "Two"
self.preferred_email = "front_desk@example.com"
self.preferred_phone = "202-555-0134"
def test_user_created_without_matching_contact(self):
"""Expect 1 Contact containing data copied from User."""
self.assertEquals(len(Contact.objects.all()), 0)
user = get_user_model().objects.create(
username=self.username,
first_name=self.first_name,
last_name=self.last_name,
email=self.email,
phone=self.phone,
)
actual = Contact.objects.get(user=user)
self.assertEquals(actual.first_name, self.first_name)
self.assertEquals(actual.last_name, self.last_name)
self.assertEquals(actual.email, self.email)
self.assertEquals(actual.phone, self.phone)
def test_user_created_with_matching_contact(self):
"""Expect 1 Contact associated, but with no data copied from User."""
self.assertEquals(len(Contact.objects.all()), 0)
Contact.objects.create(
first_name=self.preferred_first_name,
last_name=self.preferred_last_name,
email=self.email, # must be the same, to find the match!
phone=self.preferred_phone,
)
user = get_user_model().objects.create(
username=self.username,
first_name=self.first_name,
last_name=self.last_name,
email=self.email,
)
actual = Contact.objects.get(user=user)
self.assertEquals(actual.first_name, self.preferred_first_name)
self.assertEquals(actual.last_name, self.preferred_last_name)
self.assertEquals(actual.email, self.email)
self.assertEquals(actual.phone, self.preferred_phone)
def test_user_updated_without_matching_contact(self):
"""Expect 1 Contact containing data copied from User."""
# create the user
self.assertEquals(len(Contact.objects.all()), 0)
user = get_user_model().objects.create(
username=self.username, first_name="", last_name="", email="", phone=""
)
# delete the contact
Contact.objects.all().delete()
self.assertEquals(len(Contact.objects.all()), 0)
# modify the user
user.username = self.username
user.first_name = self.first_name
user.last_name = self.last_name
user.email = self.email
user.phone = self.phone
user.save()
# test
actual = Contact.objects.get(user=user)
self.assertEquals(actual.first_name, self.first_name)
self.assertEquals(actual.last_name, self.last_name)
self.assertEquals(actual.email, self.email)
self.assertEquals(actual.phone, self.phone)
def test_user_updated_with_matching_contact(self):
"""Expect 1 Contact associated, but with no data copied from User."""
# create the user
self.assertEquals(len(Contact.objects.all()), 0)
user = get_user_model().objects.create(
username=self.username,
first_name=self.first_name,
last_name=self.last_name,
email=self.email,
phone=self.phone,
)
# modify the user
user.first_name = self.preferred_first_name
user.last_name = self.preferred_last_name
user.email = self.preferred_email
user.phone = self.preferred_phone
user.save()
# test
actual = Contact.objects.get(user=user)
self.assertEquals(actual.first_name, self.first_name)
self.assertEquals(actual.last_name, self.last_name)
self.assertEquals(actual.email, self.email)
self.assertEquals(actual.phone, self.phone)

View file

@ -96,10 +96,6 @@ class LoggedInTests(TestWithUser):
self.assertContains(response, self.user.last_name)
self.assertContains(response, self.user.email)
def test_edit_profile(self):
response = self.client.get("/edit_profile/")
self.assertContains(response, "Display Name")
def test_application_form_view(self):
response = self.client.get("/register/", follow=True)
self.assertContains(
@ -152,19 +148,12 @@ class DomainApplicationTests(TestWithUser, WebTest):
type_form = type_page.form
type_form["organization_type-organization_type"] = "federal"
# test saving the page
# test next button and validate data
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = type_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/organization_type/")
type_result = type_page.form.submit()
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.organization_type, "federal")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_page.form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(type_result.status_code, 302)
@ -178,19 +167,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
federal_form = federal_page.form
federal_form["organization_federal-federal_type"] = "executive"
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = federal_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/organization_federal/")
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.federal_type, "executive")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_result = federal_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.federal_type, "executive")
# the post request should return a redirect to the next form in
# the application
self.assertEquals(federal_result.status_code, 302)
self.assertEquals(federal_result["Location"], "/register/organization_contact/")
num_pages_tested += 1
@ -212,12 +196,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
org_contact_form["organization_contact-zipcode"] = "10002"
org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
# test saving the page
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = org_contact_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/organization_contact/")
# should see results in db
org_contact_result = org_contact_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.organization_name, "Testorg")
self.assertEquals(application.address_line1, "address 1")
@ -226,11 +208,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertEquals(application.state_territory, "NY")
self.assertEquals(application.zipcode, "10002")
self.assertEquals(application.urbanization, "URB Royal Oaks")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_result = org_contact_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(org_contact_result.status_code, 302)
self.assertEquals(
org_contact_result["Location"], "/register/authorizing_official/"
@ -248,23 +227,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
ao_form["authorizing_official-email"] = "testy@town.com"
ao_form["authorizing_official-phone"] = "(201) 555 5555"
# test saving the page
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = ao_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/authorizing_official/")
# should see results in db
ao_result = ao_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.authorizing_official.first_name, "Testy ATO")
self.assertEquals(application.authorizing_official.last_name, "Tester ATO")
self.assertEquals(application.authorizing_official.title, "Chief Tester")
self.assertEquals(application.authorizing_official.email, "testy@town.com")
self.assertEquals(application.authorizing_official.phone, "(201) 555 5555")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
ao_result = ao_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(ao_result.status_code, 302)
self.assertEquals(ao_result["Location"], "/register/current_sites/")
num_pages_tested += 1
@ -276,22 +250,17 @@ class DomainApplicationTests(TestWithUser, WebTest):
current_sites_form = current_sites_page.form
current_sites_form["current_sites-0-website"] = "www.city.com"
# test saving the page
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = current_sites_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/current_sites/")
# should see results in db
current_sites_result = current_sites_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(
application.current_websites.filter(website="http://www.city.com").count(),
1,
)
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
current_sites_result = current_sites_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(current_sites_result.status_code, 302)
self.assertEquals(current_sites_result["Location"], "/register/dotgov_domain/")
num_pages_tested += 1
@ -304,21 +273,16 @@ class DomainApplicationTests(TestWithUser, WebTest):
dotgov_form["dotgov_domain-requested_domain"] = "city"
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = dotgov_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/dotgov_domain/")
# should see results in db
dotgov_result = dotgov_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.requested_domain.name, "city.gov")
self.assertEquals(
application.alternative_domains.filter(website="city1.gov").count(), 1
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
dotgov_result = dotgov_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(dotgov_result.status_code, 302)
self.assertEquals(dotgov_result["Location"], "/register/purpose/")
num_pages_tested += 1
@ -330,19 +294,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
purpose_form = purpose_page.form
purpose_form["purpose-purpose"] = "For all kinds of things."
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = purpose_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/purpose/")
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.purpose, "For all kinds of things.")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_result = purpose_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.purpose, "For all kinds of things.")
# the post request should return a redirect to the next form in
# the application
self.assertEquals(purpose_result.status_code, 302)
self.assertEquals(purpose_result["Location"], "/register/your_contact/")
num_pages_tested += 1
@ -359,23 +318,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
your_contact_form["your_contact-email"] = "testy-admin@town.com"
your_contact_form["your_contact-phone"] = "(201) 555 5556"
# test saving the page
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = your_contact_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/your_contact/")
# should see results in db
your_contact_result = your_contact_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.submitter.first_name, "Testy you")
self.assertEquals(application.submitter.last_name, "Tester you")
self.assertEquals(application.submitter.title, "Admin Tester")
self.assertEquals(application.submitter.email, "testy-admin@town.com")
self.assertEquals(application.submitter.phone, "(201) 555 5556")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
your_contact_result = your_contact_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(your_contact_result.status_code, 302)
self.assertEquals(your_contact_result["Location"], "/register/other_contacts/")
num_pages_tested += 1
@ -392,12 +346,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
other_contacts_form["other_contacts-0-email"] = "testy2@town.com"
other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557"
# test saving the page
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = other_contacts_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/other_contacts/")
# should see results in db
other_contacts_result = other_contacts_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(
application.other_contacts.filter(
@ -409,11 +361,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
).count(),
1,
)
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_result = other_contacts_form.submit()
# the post request should return a redirect to the next form in
# the application
self.assertEquals(other_contacts_result.status_code, 302)
self.assertEquals(other_contacts_result["Location"], "/register/anything_else/")
num_pages_tested += 1
@ -426,19 +375,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
anything_else_form["anything_else-anything_else"] = "Nothing else."
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = anything_else_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/anything_else/")
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.anything_else, "Nothing else.")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_result = anything_else_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.anything_else, "Nothing else.")
# the post request should return a redirect to the next form in
# the application
self.assertEquals(anything_else_result.status_code, 302)
self.assertEquals(anything_else_result["Location"], "/register/requirements/")
num_pages_tested += 1
@ -451,19 +395,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
requirements_form["requirements-is_policy_acknowledged"] = True
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = requirements_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/requirements/")
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.is_policy_acknowledged, True)
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_result = requirements_form.submit()
# validate that data from this step are being saved
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.is_policy_acknowledged, True)
# the post request should return a redirect to the next form in
# the application
self.assertEquals(requirements_result.status_code, 302)
self.assertEquals(requirements_result["Location"], "/register/review/")
num_pages_tested += 1
@ -505,12 +444,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.assertContains(review_page, "(201) 555-5557")
self.assertContains(review_page, "Nothing else.")
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = review_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/review/")
# final submission results in a redirect to the "finished" URL
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise():
@ -525,7 +458,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise():
final_result = review_result.follow()
self.assertContains(final_result, "Thank you for your domain request")
self.assertContains(final_result, "Thanks for your domain request!")
# check that any new pages are added to this test
self.assertEqual(num_pages, num_pages_tested)

View file

@ -2,5 +2,4 @@ from .application import *
from .domain import *
from .health import *
from .index import *
from .profile import *
from .whoami import *

View file

@ -1,21 +0,0 @@
from django.shortcuts import redirect, render
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from registrar.forms import EditProfileForm
@login_required
def edit_profile(request):
"""View for a profile editing page."""
if request.method == "POST":
# post to this view when changes are made
profile_form = EditProfileForm(request.POST, instance=request.user.userprofile)
if profile_form.is_valid():
profile_form.save()
messages.success(request, "Your profile is updated successfully")
return redirect(to="edit-profile")
else:
profile_form = EditProfileForm(instance=request.user.userprofile)
return render(request, "profile.html", {"profile_form": profile_form})