mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
Merge branch 'main' into nmb/rbac
This commit is contained in:
commit
2451cd8d11
29 changed files with 512 additions and 270 deletions
|
@ -4,7 +4,7 @@ Date: 2022-09-26
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Proposed
|
Superseded by [20. User models revisited, plus WHOIS](./0020-user-models-revisited-plus-whois.md)
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
|
|
|
@ -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 we’ve 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.
|
|
@ -28,18 +28,14 @@ class OpenIdConnectBackend(ModelBackend):
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
username = self.clean_username(kwargs["sub"])
|
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
|
# Some OP may actually choose to withhold some information, so we must
|
||||||
# test if it is present
|
# test if it is present
|
||||||
openid_data = {"last_login": timezone.now()}
|
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("given_name", "")
|
||||||
openid_data["first_name"] = kwargs.get("christian_name", "")
|
|
||||||
openid_data["last_name"] = kwargs.get("family_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["email"] = kwargs.get("email", "")
|
||||||
|
openid_data["phone"] = kwargs.get("phone", "")
|
||||||
|
|
||||||
# Note that this could be accomplished in one try-except clause, but
|
# 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
|
# 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):
|
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
|
||||||
args = {
|
args = {
|
||||||
UserModel.USERNAME_FIELD: username,
|
UserModel.USERNAME_FIELD: username,
|
||||||
|
# defaults _will_ be updated, these are not fallbacks
|
||||||
"defaults": openid_data,
|
"defaults": openid_data,
|
||||||
}
|
}
|
||||||
user, created = UserModel.objects.update_or_create(**args)
|
user, created = UserModel.objects.update_or_create(**args)
|
||||||
|
@ -56,10 +53,7 @@ class OpenIdConnectBackend(ModelBackend):
|
||||||
try:
|
try:
|
||||||
user = UserModel.objects.get_by_natural_key(username)
|
user = UserModel.objects.get_by_natural_key(username)
|
||||||
except UserModel.DoesNotExist:
|
except UserModel.DoesNotExist:
|
||||||
try:
|
return None
|
||||||
user = UserModel.objects.get(email=kwargs["email"])
|
|
||||||
except UserModel.DoesNotExist:
|
|
||||||
return None
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def clean_username(self, username):
|
def clean_username(self, username):
|
||||||
|
|
|
@ -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."""
|
"""Edit a user's profile on the user page."""
|
||||||
|
|
||||||
model = models.UserProfile
|
model = models.Contact
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(UserAdmin):
|
class MyUserAdmin(UserAdmin):
|
||||||
|
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
inlines = [UserProfileInline]
|
inlines = [UserContactInline]
|
||||||
|
|
||||||
|
|
||||||
class HostIPInline(admin.StackedInline):
|
class HostIPInline(admin.StackedInline):
|
||||||
|
|
|
@ -645,7 +645,6 @@ if DEBUG:
|
||||||
NPLUSONE_RAISE = False
|
NPLUSONE_RAISE = False
|
||||||
NPLUSONE_WHITELIST = [
|
NPLUSONE_WHITELIST = [
|
||||||
{"model": "admin.LogEntry", "field": "user"},
|
{"model": "admin.LogEntry", "field": "user"},
|
||||||
{"model": "registrar.UserProfile"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# insert the amazing django-debug-toolbar
|
# insert the amazing django-debug-toolbar
|
||||||
|
|
|
@ -53,7 +53,6 @@ urlpatterns = [
|
||||||
name=views.ApplicationWizard.EDIT_URL_NAME,
|
name=views.ApplicationWizard.EDIT_URL_NAME,
|
||||||
),
|
),
|
||||||
path("health/", views.health),
|
path("health/", views.health),
|
||||||
path("edit_profile/", views.edit_profile, name="edit-profile"),
|
|
||||||
path("openid/", include("djangooidc.urls")),
|
path("openid/", include("djangooidc.urls")),
|
||||||
path("register/", include((application_urls, APPLICATION_NAMESPACE))),
|
path("register/", include((application_urls, APPLICATION_NAMESPACE))),
|
||||||
path("api/v1/available/<domain>", available, name="available"),
|
path("api/v1/available/<domain>", available, name="available"),
|
||||||
|
|
|
@ -4,7 +4,6 @@ from faker import Faker
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
User,
|
User,
|
||||||
UserProfile,
|
|
||||||
DomainApplication,
|
DomainApplication,
|
||||||
Domain,
|
Domain,
|
||||||
Contact,
|
Contact,
|
||||||
|
@ -57,8 +56,6 @@ class UserFixture:
|
||||||
user.is_active = True
|
user.is_active = True
|
||||||
user.save()
|
user.save()
|
||||||
logger.debug("User object created for %s" % admin["first_name"])
|
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:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
logger.debug("All users loaded.")
|
logger.debug("All users loaded.")
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
from .edit_profile import *
|
|
||||||
from .application_wizard import *
|
from .application_wizard import *
|
||||||
|
|
|
@ -226,8 +226,10 @@ class OrganizationElectionForm(RegistrarForm):
|
||||||
is_election_board = self.cleaned_data["is_election_board"]
|
is_election_board = self.cleaned_data["is_election_board"]
|
||||||
if is_election_board is None:
|
if is_election_board is None:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
"Select “Yes” if you represent an election office. Select “No” if you"
|
(
|
||||||
" don’t.",
|
"Select “Yes” if you represent an election office. Select “No” if"
|
||||||
|
" you don’t."
|
||||||
|
),
|
||||||
code="required",
|
code="required",
|
||||||
)
|
)
|
||||||
return is_election_board
|
return is_election_board
|
||||||
|
@ -399,6 +401,12 @@ class CurrentSitesForm(RegistrarForm):
|
||||||
website = forms.URLField(
|
website = forms.URLField(
|
||||||
required=False,
|
required=False,
|
||||||
label="Public website",
|
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 has to end in a space to get the label_suffix to show
|
||||||
label=(
|
label=(
|
||||||
"Please explain why there are no other employees from your organization"
|
"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(),
|
widget=forms.Textarea(),
|
||||||
)
|
)
|
||||||
|
@ -692,7 +700,7 @@ class AnythingElseForm(RegistrarForm):
|
||||||
|
|
||||||
class RequirementsForm(RegistrarForm):
|
class RequirementsForm(RegistrarForm):
|
||||||
is_policy_acknowledged = forms.BooleanField(
|
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={
|
error_messages={
|
||||||
"required": (
|
"required": (
|
||||||
"Check the box if you read and agree to the requirements for"
|
"Check the box if you read and agree to the requirements for"
|
||||||
|
|
|
@ -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"]
|
|
17
src/registrar/migrations/0012_delete_userprofile.py
Normal file
17
src/registrar/migrations/0012_delete_userprofile.py
Normal 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",
|
||||||
|
),
|
||||||
|
]
|
68
src/registrar/migrations/0013_publiccontact_contact_user.py
Normal file
68
src/registrar/migrations/0013_publiccontact_contact_user.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -7,7 +7,7 @@ import django.db.models.deletion
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("registrar", "0011_remove_domainapplication_security_email"),
|
("registrar", "0014_user_phone_alter_contact_user"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
|
@ -7,7 +7,7 @@ from .host_ip import HostIP
|
||||||
from .host import Host
|
from .host import Host
|
||||||
from .nameserver import Nameserver
|
from .nameserver import Nameserver
|
||||||
from .user_domain_role import UserDomainRole
|
from .user_domain_role import UserDomainRole
|
||||||
from .user_profile import UserProfile
|
from .public_contact import PublicContact
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ __all__ = [
|
||||||
"Host",
|
"Host",
|
||||||
"Nameserver",
|
"Nameserver",
|
||||||
"UserDomainRole",
|
"UserDomainRole",
|
||||||
"UserProfile",
|
"PublicContact",
|
||||||
"User",
|
"User",
|
||||||
"Website",
|
"Website",
|
||||||
]
|
]
|
||||||
|
@ -31,6 +31,6 @@ auditlog.register(HostIP)
|
||||||
auditlog.register(Host)
|
auditlog.register(Host)
|
||||||
auditlog.register(Nameserver)
|
auditlog.register(Nameserver)
|
||||||
auditlog.register(UserDomainRole)
|
auditlog.register(UserDomainRole)
|
||||||
auditlog.register(UserProfile)
|
auditlog.register(PublicContact)
|
||||||
auditlog.register(User)
|
auditlog.register(User)
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
|
|
|
@ -9,6 +9,13 @@ class Contact(TimeStampedModel):
|
||||||
|
|
||||||
"""Contact information follows a similar pattern for each contact."""
|
"""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(
|
first_name = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
|
50
src/registrar/models/public_contact.py
Normal file
50
src/registrar/models/public_contact.py
Normal 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}>"
|
|
@ -1,6 +1,8 @@
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
|
@ -14,6 +16,13 @@ class User(AbstractUser):
|
||||||
related_name="users",
|
related_name="users",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
phone = PhoneNumberField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Phone",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
# this info is pulled from Login.gov
|
# this info is pulled from Login.gov
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
|
|
|
@ -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"
|
|
|
@ -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
|
|
|
@ -5,7 +5,7 @@ from django.core.management import call_command
|
||||||
from django.db.models.signals import post_save, post_migrate
|
from django.db.models.signals import post_save, post_migrate
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .models import User, UserProfile
|
from .models import User, Contact
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -15,18 +15,46 @@ logger = logging.getLogger(__name__)
|
||||||
def handle_profile(sender, instance, **kwargs):
|
def handle_profile(sender, instance, **kwargs):
|
||||||
"""Method for when a User is saved.
|
"""Method for when a User is saved.
|
||||||
|
|
||||||
If the user is being created, then create a matching UserProfile. Otherwise
|
A first time registrant may have been invited, so we'll search for a matching
|
||||||
save an updated profile or create one if it doesn't exist.
|
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):
|
first_name = getattr(instance, "first_name", "")
|
||||||
UserProfile.objects.create(user=instance)
|
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:
|
else:
|
||||||
# the user is not being created.
|
contacts = Contact.objects.filter(user=instance)
|
||||||
if hasattr(instance, "userprofile"):
|
|
||||||
instance.userprofile.save()
|
if len(contacts) == 0: # no matching contact
|
||||||
else:
|
Contact.objects.create(
|
||||||
UserProfile.objects.create(user=instance)
|
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)
|
@receiver(post_migrate)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}Thank you for your domain request{% endblock %}
|
{% block title %}Thanks for your domain request!{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container register-form-step">
|
<main id="main-content" class="grid-container register-form-step">
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
/>
|
/>
|
||||||
<h1>Thank you</h1>
|
<h1>Thank you</h1>
|
||||||
</span>
|
</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>
|
your authorizing official, and any contacts you added.</p>
|
||||||
|
|
||||||
<h2>Next steps in this process</h2>
|
<h2>Next steps in this process</h2>
|
||||||
|
|
|
@ -70,12 +70,6 @@
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
>Submit your domain request</button>
|
>Submit your domain request</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
name="submit_button"
|
|
||||||
value="save"
|
|
||||||
class="usa-button usa-button--outline"
|
|
||||||
>Save progress</button>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
{% load field_helpers %}
|
{% load field_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p> We’ll use the following information to contact you about your domain request and,
|
<p>We’ll use this information to contact you about your domain request.</p>
|
||||||
once your request is approved, about managing your domain.</p>
|
|
||||||
|
|
||||||
<p>If you’d like us to use a different name, email, or phone number you can make those
|
<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
|
changes below. Changing your contact information here won’t affect your login.gov
|
||||||
|
@ -35,4 +34,4 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -29,7 +29,13 @@ class TestFormValidation(TestCase):
|
||||||
|
|
||||||
def test_website_invalid(self):
|
def test_website_invalid(self):
|
||||||
form = CurrentSitesForm(data={"website": "nah"})
|
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):
|
def test_website_valid(self):
|
||||||
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
|
form = CurrentSitesForm(data={"website": "hyphens-rule.gov.uk"})
|
||||||
|
@ -83,7 +89,7 @@ class TestFormValidation(TestCase):
|
||||||
"""Must be a valid phone number."""
|
"""Must be a valid phone number."""
|
||||||
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})
|
form = AuthorizingOfficialForm(data={"phone": "boss@boss"})
|
||||||
self.assertTrue(
|
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):
|
def test_your_contact_email_invalid(self):
|
||||||
|
@ -98,7 +104,7 @@ class TestFormValidation(TestCase):
|
||||||
"""Must be a valid phone number."""
|
"""Must be a valid phone number."""
|
||||||
form = YourContactForm(data={"phone": "boss@boss"})
|
form = YourContactForm(data={"phone": "boss@boss"})
|
||||||
self.assertTrue(
|
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):
|
def test_other_contact_email_invalid(self):
|
||||||
|
@ -113,7 +119,7 @@ class TestFormValidation(TestCase):
|
||||||
"""Must be a valid phone number."""
|
"""Must be a valid phone number."""
|
||||||
form = OtherContactsForm(data={"phone": "boss@boss"})
|
form = OtherContactsForm(data={"phone": "boss@boss"})
|
||||||
self.assertTrue(
|
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):
|
def test_requirements_form_blank(self):
|
||||||
|
|
103
src/registrar/tests/test_signals.py
Normal file
103
src/registrar/tests/test_signals.py
Normal 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)
|
|
@ -96,10 +96,6 @@ class LoggedInTests(TestWithUser):
|
||||||
self.assertContains(response, self.user.last_name)
|
self.assertContains(response, self.user.last_name)
|
||||||
self.assertContains(response, self.user.email)
|
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):
|
def test_application_form_view(self):
|
||||||
response = self.client.get("/register/", follow=True)
|
response = self.client.get("/register/", follow=True)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
|
@ -152,19 +148,12 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
type_form = type_page.form
|
type_form = type_page.form
|
||||||
type_form["organization_type-organization_type"] = "federal"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = type_page.form.submit("submit_button", value="save")
|
type_result = type_page.form.submit()
|
||||||
# should remain on the same page
|
|
||||||
self.assertEquals(result["Location"], "/register/organization_type/")
|
|
||||||
# should see results in db
|
# should see results in db
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(application.organization_type, "federal")
|
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 post request should return a redirect to the next form in
|
||||||
# the application
|
# the application
|
||||||
self.assertEquals(type_result.status_code, 302)
|
self.assertEquals(type_result.status_code, 302)
|
||||||
|
@ -178,19 +167,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
federal_form = federal_page.form
|
federal_form = federal_page.form
|
||||||
federal_form["organization_federal-federal_type"] = "executive"
|
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
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
federal_result = federal_form.submit()
|
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.status_code, 302)
|
||||||
self.assertEquals(federal_result["Location"], "/register/organization_contact/")
|
self.assertEquals(federal_result["Location"], "/register/organization_contact/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -212,12 +196,10 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
org_contact_form["organization_contact-zipcode"] = "10002"
|
org_contact_form["organization_contact-zipcode"] = "10002"
|
||||||
org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = org_contact_page.form.submit("submit_button", value="save")
|
org_contact_result = org_contact_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/organization_contact/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(application.organization_name, "Testorg")
|
self.assertEquals(application.organization_name, "Testorg")
|
||||||
self.assertEquals(application.address_line1, "address 1")
|
self.assertEquals(application.address_line1, "address 1")
|
||||||
|
@ -226,11 +208,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.assertEquals(application.state_territory, "NY")
|
self.assertEquals(application.state_territory, "NY")
|
||||||
self.assertEquals(application.zipcode, "10002")
|
self.assertEquals(application.zipcode, "10002")
|
||||||
self.assertEquals(application.urbanization, "URB Royal Oaks")
|
self.assertEquals(application.urbanization, "URB Royal Oaks")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
# test next button
|
# the application
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
org_contact_result = org_contact_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(org_contact_result.status_code, 302)
|
self.assertEquals(org_contact_result.status_code, 302)
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
org_contact_result["Location"], "/register/authorizing_official/"
|
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-email"] = "testy@town.com"
|
||||||
ao_form["authorizing_official-phone"] = "(201) 555 5555"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = ao_page.form.submit("submit_button", value="save")
|
ao_result = ao_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/authorizing_official/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(application.authorizing_official.first_name, "Testy ATO")
|
self.assertEquals(application.authorizing_official.first_name, "Testy ATO")
|
||||||
self.assertEquals(application.authorizing_official.last_name, "Tester ATO")
|
self.assertEquals(application.authorizing_official.last_name, "Tester ATO")
|
||||||
self.assertEquals(application.authorizing_official.title, "Chief Tester")
|
self.assertEquals(application.authorizing_official.title, "Chief Tester")
|
||||||
self.assertEquals(application.authorizing_official.email, "testy@town.com")
|
self.assertEquals(application.authorizing_official.email, "testy@town.com")
|
||||||
self.assertEquals(application.authorizing_official.phone, "(201) 555 5555")
|
self.assertEquals(application.authorizing_official.phone, "(201) 555 5555")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
# test next button
|
# the application
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
ao_result = ao_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(ao_result.status_code, 302)
|
self.assertEquals(ao_result.status_code, 302)
|
||||||
self.assertEquals(ao_result["Location"], "/register/current_sites/")
|
self.assertEquals(ao_result["Location"], "/register/current_sites/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -276,22 +250,17 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
current_sites_form = current_sites_page.form
|
current_sites_form = current_sites_page.form
|
||||||
current_sites_form["current_sites-0-website"] = "www.city.com"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = current_sites_page.form.submit("submit_button", value="save")
|
current_sites_result = current_sites_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/current_sites/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
application.current_websites.filter(website="http://www.city.com").count(),
|
application.current_websites.filter(website="http://www.city.com").count(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
# test next button
|
# the application
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
current_sites_result = current_sites_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(current_sites_result.status_code, 302)
|
self.assertEquals(current_sites_result.status_code, 302)
|
||||||
self.assertEquals(current_sites_result["Location"], "/register/dotgov_domain/")
|
self.assertEquals(current_sites_result["Location"], "/register/dotgov_domain/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -304,21 +273,16 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
dotgov_form["dotgov_domain-requested_domain"] = "city"
|
dotgov_form["dotgov_domain-requested_domain"] = "city"
|
||||||
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
|
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
|
||||||
|
|
||||||
# test saving the page
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = dotgov_page.form.submit("submit_button", value="save")
|
dotgov_result = dotgov_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/dotgov_domain/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(application.requested_domain.name, "city.gov")
|
self.assertEquals(application.requested_domain.name, "city.gov")
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
application.alternative_domains.filter(website="city1.gov").count(), 1
|
application.alternative_domains.filter(website="city1.gov").count(), 1
|
||||||
)
|
)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
# the application
|
||||||
dotgov_result = dotgov_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(dotgov_result.status_code, 302)
|
self.assertEquals(dotgov_result.status_code, 302)
|
||||||
self.assertEquals(dotgov_result["Location"], "/register/purpose/")
|
self.assertEquals(dotgov_result["Location"], "/register/purpose/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -330,19 +294,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
purpose_form = purpose_page.form
|
purpose_form = purpose_page.form
|
||||||
purpose_form["purpose-purpose"] = "For all kinds of things."
|
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
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
purpose_result = purpose_form.submit()
|
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.status_code, 302)
|
||||||
self.assertEquals(purpose_result["Location"], "/register/your_contact/")
|
self.assertEquals(purpose_result["Location"], "/register/your_contact/")
|
||||||
num_pages_tested += 1
|
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-email"] = "testy-admin@town.com"
|
||||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = your_contact_page.form.submit("submit_button", value="save")
|
your_contact_result = your_contact_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/your_contact/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(application.submitter.first_name, "Testy you")
|
self.assertEquals(application.submitter.first_name, "Testy you")
|
||||||
self.assertEquals(application.submitter.last_name, "Tester you")
|
self.assertEquals(application.submitter.last_name, "Tester you")
|
||||||
self.assertEquals(application.submitter.title, "Admin Tester")
|
self.assertEquals(application.submitter.title, "Admin Tester")
|
||||||
self.assertEquals(application.submitter.email, "testy-admin@town.com")
|
self.assertEquals(application.submitter.email, "testy-admin@town.com")
|
||||||
self.assertEquals(application.submitter.phone, "(201) 555 5556")
|
self.assertEquals(application.submitter.phone, "(201) 555 5556")
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
# test next button
|
# the application
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
your_contact_result = your_contact_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(your_contact_result.status_code, 302)
|
self.assertEquals(your_contact_result.status_code, 302)
|
||||||
self.assertEquals(your_contact_result["Location"], "/register/other_contacts/")
|
self.assertEquals(your_contact_result["Location"], "/register/other_contacts/")
|
||||||
num_pages_tested += 1
|
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-email"] = "testy2@town.com"
|
||||||
other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557"
|
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)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
result = other_contacts_page.form.submit("submit_button", value="save")
|
other_contacts_result = other_contacts_form.submit()
|
||||||
# should remain on the same page
|
# validate that data from this step are being saved
|
||||||
self.assertEquals(result["Location"], "/register/other_contacts/")
|
|
||||||
# should see results in db
|
|
||||||
application = DomainApplication.objects.get() # there's only one
|
application = DomainApplication.objects.get() # there's only one
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
application.other_contacts.filter(
|
application.other_contacts.filter(
|
||||||
|
@ -409,11 +361,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
).count(),
|
).count(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
# the post request should return a redirect to the next form in
|
||||||
# test next button
|
# the application
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
other_contacts_result = other_contacts_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(other_contacts_result.status_code, 302)
|
self.assertEquals(other_contacts_result.status_code, 302)
|
||||||
self.assertEquals(other_contacts_result["Location"], "/register/anything_else/")
|
self.assertEquals(other_contacts_result["Location"], "/register/anything_else/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -426,19 +375,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
anything_else_form["anything_else-anything_else"] = "Nothing else."
|
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
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
anything_else_result = anything_else_form.submit()
|
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.status_code, 302)
|
||||||
self.assertEquals(anything_else_result["Location"], "/register/requirements/")
|
self.assertEquals(anything_else_result["Location"], "/register/requirements/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -451,19 +395,14 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
requirements_form["requirements-is_policy_acknowledged"] = True
|
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
|
# test next button
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
requirements_result = requirements_form.submit()
|
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.status_code, 302)
|
||||||
self.assertEquals(requirements_result["Location"], "/register/review/")
|
self.assertEquals(requirements_result["Location"], "/register/review/")
|
||||||
num_pages_tested += 1
|
num_pages_tested += 1
|
||||||
|
@ -505,12 +444,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.assertContains(review_page, "(201) 555-5557")
|
self.assertContains(review_page, "(201) 555-5557")
|
||||||
self.assertContains(review_page, "Nothing else.")
|
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
|
# final submission results in a redirect to the "finished" URL
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
|
@ -525,7 +458,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
final_result = review_result.follow()
|
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
|
# check that any new pages are added to this test
|
||||||
self.assertEqual(num_pages, num_pages_tested)
|
self.assertEqual(num_pages, num_pages_tested)
|
||||||
|
|
|
@ -2,5 +2,4 @@ from .application import *
|
||||||
from .domain import *
|
from .domain import *
|
||||||
from .health import *
|
from .health import *
|
||||||
from .index import *
|
from .index import *
|
||||||
from .profile import *
|
|
||||||
from .whoami import *
|
from .whoami import *
|
||||||
|
|
|
@ -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})
|
|
Loading…
Add table
Add a link
Reference in a new issue