Save button for domain application

This commit is contained in:
Seamus Johnston 2022-11-28 07:55:57 -06:00
parent 19c360f3bf
commit 07eb374d25
No known key found for this signature in database
GPG key ID: 2F21225985069105
21 changed files with 979 additions and 312 deletions

View file

@ -1,7 +1,12 @@
[mypy]
plugins =
mypy_django_plugin.main
# strict_optional: treat None as compatible with every type?
# `var: int` is equal to `var: int|None`
strict_optional = True
# implicit_optional: treat arguments a None default value as implicitly Optional?
# `var: int = None` is equal to `var: Optional[int] = None`
implicit_optional = True
[mypy.plugins.django-stubs]
django_settings_module = "registrar.config.settings"

View file

@ -24,6 +24,7 @@ urlpatterns = [
path("", index.index, name="home"),
path("whoami/", whoami.whoami, name="whoami"),
path("admin/", admin.site.urls),
path("application/<id>/edit/", application_wizard, name="edit-application"),
path("health/", health.health),
path("edit_profile/", profile.edit_profile, name="edit-profile"),
path("openid/", include("djangooidc.urls")),

View file

@ -6,52 +6,46 @@ import logging
from django import forms
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import resolve
from formtools.wizard.views import NamedUrlSessionWizardView # type: ignore
from registrar.models import DomainApplication, Domain
from registrar.models import Contact, DomainApplication, Domain
logger = logging.getLogger(__name__)
# Subclass used to remove the default colon suffix from all fields
class RegistrarForm(forms.Form):
"""Subclass used to remove the default colon suffix from all fields."""
def __init__(self, *args, **kwargs):
kwargs.setdefault("label_suffix", "")
super(RegistrarForm, self).__init__(*args, **kwargs)
def to_database(self, obj: DomainApplication | Contact):
"""
Adds this form's cleaned data to `obj` and saves `obj`.
Does nothing if form is not valid.
"""
if not self.is_valid():
return
for name, value in self.cleaned_data.items():
setattr(obj, name, value)
obj.save()
def from_database(self, obj: DomainApplication | Contact):
"""Initializes this form's fields with values gotten from `obj`."""
for name in self.declared_fields.keys():
self.initial[name] = getattr(obj, name) # type: ignore
class OrganizationTypeForm(RegistrarForm):
organization_type = forms.ChoiceField(
required=True,
choices=[
("Federal", "Federal: a federal agency"),
("Interstate", "Interstate: an organization of two or more states"),
(
"State_or_Territory",
(
"State or Territory: One of the 50 U.S. states, the District of "
"Columbia, American Samoa, Guam, Northern Mariana Islands, "
"Puerto Rico, or the U.S. Virgin Islands"
),
),
(
"Tribal",
(
"Tribal: a tribal government recognized by the federal or "
"state government"
),
),
("County", "County: a county, parish, or borough"),
("City", "City: a city, town, township, village, etc."),
(
"Special_District",
"Special District: an independent organization within a single state",
),
],
choices=DomainApplication.OrganizationChoices.choices,
widget=forms.RadioSelect,
)
@ -59,20 +53,20 @@ class OrganizationTypeForm(RegistrarForm):
class OrganizationFederalForm(RegistrarForm):
federal_type = forms.ChoiceField(
required=False,
choices=DomainApplication.BRANCH_CHOICES,
choices=DomainApplication.BranchChoices.choices,
widget=forms.RadioSelect,
)
class OrganizationElectionForm(RegistrarForm):
is_election_board = forms.BooleanField(
required=False,
widget=forms.RadioSelect(
choices=[
(True, "Yes"),
(False, "No"),
],
),
required=False,
)
@ -83,71 +77,32 @@ class OrganizationContactForm(RegistrarForm):
required=False,
label="Address line 2",
)
us_state = forms.ChoiceField(
label="State",
choices=[
("AL", "Alabama"),
("AK", "Alaska"),
("AZ", "Arizona"),
("AR", "Arkansas"),
("CA", "California"),
("CO", "Colorado"),
("CT", "Connecticut"),
("DE", "Delaware"),
("DC", "District of Columbia"),
("FL", "Florida"),
("GA", "Georgia"),
("HI", "Hawaii"),
("ID", "Idaho"),
("IL", "Illinois"),
("IN", "Indiana"),
("IA", "Iowa"),
("KS", "Kansas"),
("KY", "Kentucky"),
("LA", "Louisiana"),
("ME", "Maine"),
("MD", "Maryland"),
("MA", "Massachusetts"),
("MI", "Michigan"),
("MN", "Minnesota"),
("MS", "Mississippi"),
("MO", "Missouri"),
("MT", "Montana"),
("NE", "Nebraska"),
("NV", "Nevada"),
("NH", "New Hampshire"),
("NJ", "New Jersey"),
("NM", "New Mexico"),
("NY", "New York"),
("NC", "North Carolina"),
("ND", "North Dakota"),
("OH", "Ohio"),
("OK", "Oklahoma"),
("OR", "Oregon"),
("PA", "Pennsylvania"),
("RI", "Rhode Island"),
("SC", "South Carolina"),
("SD", "South Dakota"),
("TN", "Tennessee"),
("TX", "Texas"),
("UT", "Utah"),
("VT", "Vermont"),
("VA", "Virginia"),
("WA", "Washington"),
("WV", "West Virginia"),
("WI", "Wisconsin"),
("WY", "Wyoming"),
("AS", "American Samoa"),
("GU", "Guam"),
("MP", "Northern Mariana Islands"),
("PR", "Puerto Rico"),
("VI", "Virgin Islands"),
],
state_territory = forms.ChoiceField(
label="State", choices=DomainApplication.StateTerritoryChoices.choices
)
zipcode = forms.CharField(label="ZIP code")
class AuthorizingOfficialForm(RegistrarForm):
def to_database(self, obj):
"""Adds this form's cleaned data to `obj` and saves `obj`."""
if not self.is_valid():
return
contact = getattr(obj, "authorizing_official", None)
if contact is not None:
super().to_database(contact)
else:
contact = Contact()
super().to_database(contact)
obj.authorizing_official = contact
obj.save()
def from_database(self, obj):
"""Initializes this form's fields with values gotten from `obj`."""
contact = getattr(obj, "authorizing_official", None)
if contact is not None:
super().from_database(contact)
first_name = forms.CharField(label="First name/given name")
middle_name = forms.CharField(
required=False,
@ -160,6 +115,21 @@ class AuthorizingOfficialForm(RegistrarForm):
class CurrentSitesForm(RegistrarForm):
def to_database(self, obj):
"""Adds this form's cleaned data to `obj` and saves `obj`."""
if not self.is_valid():
return
obj.save()
normalized = Domain.normalize(self.cleaned_data["current_site"])
# TODO: ability to update existing records
obj.current_websites.create(website=normalized)
def from_database(self, obj):
"""Initializes this form's fields with values gotten from `obj`."""
current_website = obj.current_websites.first()
if current_website is not None:
self.initial["current_site"] = current_website.website
current_site = forms.CharField(
required=False,
label="Enter your organizations public website, if you have one. For example, "
@ -168,7 +138,36 @@ class CurrentSitesForm(RegistrarForm):
class DotGovDomainForm(RegistrarForm):
dotgov_domain = forms.CharField(label="What .gov domain do you want?")
def to_database(self, obj):
"""Adds this form's cleaned data to `obj` and saves `obj`."""
if not self.is_valid():
return
normalized = Domain.normalize(self.cleaned_data["requested_domain"], "gov")
requested_domain = getattr(obj, "requested_domain", None)
if requested_domain is not None:
requested_domain.name = normalized
requested_domain.save()
else:
requested_domain = Domain.objects.create(name=normalized)
obj.requested_domain = requested_domain
obj.save()
obj.save()
normalized = Domain.normalize(self.cleaned_data["alternative_domain"], "gov")
# TODO: ability to update existing records
obj.alternative_domains.create(website=normalized)
def from_database(self, obj):
"""Initializes this form's fields with values gotten from `obj`."""
requested_domain = getattr(obj, "requested_domain", None)
if requested_domain is not None:
self.initial["requested_domain"] = requested_domain.sld
alternative_domain = obj.alternative_domains.first()
if alternative_domain is not None:
self.initial["alternative_domain"] = alternative_domain.sld
requested_domain = forms.CharField(label="What .gov domain do you want?")
alternative_domain = forms.CharField(
required=False,
label="Are there other domains youd like if we cant give you your first "
@ -177,10 +176,29 @@ class DotGovDomainForm(RegistrarForm):
class PurposeForm(RegistrarForm):
purpose_field = forms.CharField(label="Purpose", widget=forms.Textarea())
purpose = forms.CharField(label="Purpose", widget=forms.Textarea())
class YourContactForm(RegistrarForm):
def to_database(self, obj):
"""Adds this form's cleaned data to `obj` and saves `obj`."""
if not self.is_valid():
return
contact = getattr(obj, "submitter", None)
if contact is not None:
super().to_database(contact)
else:
contact = Contact()
super().to_database(contact)
obj.submitter = contact
obj.save()
def from_database(self, obj):
"""Initializes this form's fields with values gotten from `obj`."""
contact = getattr(obj, "submitter", None)
if contact is not None:
super().from_database(contact)
first_name = forms.CharField(label="First name/given name")
middle_name = forms.CharField(
required=False,
@ -193,6 +211,27 @@ class YourContactForm(RegistrarForm):
class OtherContactsForm(RegistrarForm):
def to_database(self, obj):
"""Adds this form's cleaned data to `obj` and saves `obj`."""
if not self.is_valid():
return
obj.save()
# TODO: ability to handle multiple contacts
contact = obj.other_contacts.filter(email=self.cleaned_data["email"]).first()
if contact is not None:
super().to_database(contact)
else:
contact = Contact()
super().to_database(contact)
obj.other_contacts.add(contact)
def from_database(self, obj):
"""Initializes this form's fields with values gotten from `obj`."""
other_contacts = obj.other_contacts.first()
if other_contacts is not None:
super().from_database(other_contacts)
first_name = forms.CharField(label="First name/given name")
middle_name = forms.CharField(
required=False,
@ -205,7 +244,7 @@ class OtherContactsForm(RegistrarForm):
class SecurityEmailForm(RegistrarForm):
email = forms.EmailField(
security_email = forms.EmailField(
required=False,
label="Security email",
)
@ -218,72 +257,97 @@ class AnythingElseForm(RegistrarForm):
class RequirementsForm(RegistrarForm):
agree_check = forms.BooleanField(
is_policy_acknowledged = forms.BooleanField(
label="I read and agree to the .gov domain requirements."
)
# Empty class for the review page which gets included as part of the form, but does not
# have any form fields itself
class ReviewForm(RegistrarForm):
"""
Empty class for the review page.
It gets included as part of the form, but does not have any form fields itself.
"""
def to_database(self, _):
"""This form has no data. Do nothing."""
pass
pass
# List of forms in our wizard. Each entry is a tuple of a name and a form
# subclass
class Step:
"""Names for each page of the application wizard."""
ORGANIZATION_TYPE = "organization_type"
ORGANIZATION_FEDERAL = "organization_federal"
ORGANIZATION_ELECTION = "organization_election"
ORGANIZATION_CONTACT = "organization_contact"
AUTHORIZING_OFFICIAL = "authorizing_official"
CURRENT_SITES = "current_sites"
DOTGOV_DOMAIN = "dotgov_domain"
PURPOSE = "purpose"
YOUR_CONTACT = "your_contact"
OTHER_CONTACTS = "other_contacts"
SECURITY_EMAIL = "security_email"
ANYTHING_ELSE = "anything_else"
REQUIREMENTS = "requirements"
REVIEW = "review"
# List of forms in our wizard.
# Each entry is a tuple of a name and a form subclass
FORMS = [
("organization_type", OrganizationTypeForm),
("organization_federal", OrganizationFederalForm),
("organization_election", OrganizationElectionForm),
("organization_contact", OrganizationContactForm),
("authorizing_official", AuthorizingOfficialForm),
("current_sites", CurrentSitesForm),
("dotgov_domain", DotGovDomainForm),
("purpose", PurposeForm),
("your_contact", YourContactForm),
("other_contacts", OtherContactsForm),
("security_email", SecurityEmailForm),
("anything_else", AnythingElseForm),
("requirements", RequirementsForm),
("review", ReviewForm),
(Step.ORGANIZATION_TYPE, OrganizationTypeForm),
(Step.ORGANIZATION_FEDERAL, OrganizationFederalForm),
(Step.ORGANIZATION_ELECTION, OrganizationElectionForm),
(Step.ORGANIZATION_CONTACT, OrganizationContactForm),
(Step.AUTHORIZING_OFFICIAL, AuthorizingOfficialForm),
(Step.CURRENT_SITES, CurrentSitesForm),
(Step.DOTGOV_DOMAIN, DotGovDomainForm),
(Step.PURPOSE, PurposeForm),
(Step.YOUR_CONTACT, YourContactForm),
(Step.OTHER_CONTACTS, OtherContactsForm),
(Step.SECURITY_EMAIL, SecurityEmailForm),
(Step.ANYTHING_ELSE, AnythingElseForm),
(Step.REQUIREMENTS, RequirementsForm),
(Step.REVIEW, ReviewForm),
]
# Dict to match up the right template with the right step. Keys here must
# match the first elements of the tuples in FORMS
# Dict to match up the right template with the right step.
TEMPLATES = {
"organization_type": "application_org_type.html",
"organization_federal": "application_org_federal.html",
"organization_election": "application_org_election.html",
"organization_contact": "application_org_contact.html",
"authorizing_official": "application_authorizing_official.html",
"current_sites": "application_current_sites.html",
"dotgov_domain": "application_dotgov_domain.html",
"purpose": "application_purpose.html",
"your_contact": "application_your_contact.html",
"other_contacts": "application_other_contacts.html",
"security_email": "application_security_email.html",
"anything_else": "application_anything_else.html",
"requirements": "application_requirements.html",
"review": "application_review.html",
Step.ORGANIZATION_TYPE: "application_org_type.html",
Step.ORGANIZATION_FEDERAL: "application_org_federal.html",
Step.ORGANIZATION_ELECTION: "application_org_election.html",
Step.ORGANIZATION_CONTACT: "application_org_contact.html",
Step.AUTHORIZING_OFFICIAL: "application_authorizing_official.html",
Step.CURRENT_SITES: "application_current_sites.html",
Step.DOTGOV_DOMAIN: "application_dotgov_domain.html",
Step.PURPOSE: "application_purpose.html",
Step.YOUR_CONTACT: "application_your_contact.html",
Step.OTHER_CONTACTS: "application_other_contacts.html",
Step.SECURITY_EMAIL: "application_security_email.html",
Step.ANYTHING_ELSE: "application_anything_else.html",
Step.REQUIREMENTS: "application_requirements.html",
Step.REVIEW: "application_review.html",
}
# We need to pass our page titles as context to the templates, indexed
# by the step names
# We need to pass our page titles as context to the templates
TITLES = {
"organization_type": "Type of organization",
"organization_federal": "Type of organization — Federal",
"organization_election": "Type of organization — Election board",
"organization_contact": "Organization name and mailing address",
"authorizing_official": "Authorizing official",
"current_sites": "Organization website",
"dotgov_domain": ".gov domain",
"purpose": "Purpose of your domain",
"your_contact": "Your contact information",
"other_contacts": "Other contacts for your domain",
"security_email": "Security email for public use",
"anything_else": "Anything else we should know?",
"requirements": "Requirements for registration and operation of .gov domains",
"review": "Review and submit your domain request",
Step.ORGANIZATION_TYPE: "Type of organization",
Step.ORGANIZATION_FEDERAL: "Type of organization — Federal",
Step.ORGANIZATION_ELECTION: "Type of organization — Election board",
Step.ORGANIZATION_CONTACT: "Organization name and mailing address",
Step.AUTHORIZING_OFFICIAL: "Authorizing official",
Step.CURRENT_SITES: "Organization website",
Step.DOTGOV_DOMAIN: ".gov domain",
Step.PURPOSE: "Purpose of your domain",
Step.YOUR_CONTACT: "Your contact information",
Step.OTHER_CONTACTS: "Other contacts for your domain",
Step.SECURITY_EMAIL: "Security email for public use",
Step.ANYTHING_ELSE: "Anything else we should know?",
Step.REQUIREMENTS: "Requirements for registration and operation of .gov domains",
Step.REVIEW: "Review and submit your domain request",
}
@ -303,6 +367,10 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
domain applications. Each form in the sequence has its own URL and
the progress through the form is stored in the Django session (thus
"NamedUrlSessionWizardView").
Caution: due to the redirect performed by using NamedUrlSessionWizardView,
many methods, such as `process_step`, are called TWICE per request. For
this reason, methods in this class need to be idempotent.
"""
form_list = FORMS
@ -320,42 +388,97 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
context["form_titles"] = TITLES
return context
def forms_to_object(self, form_dict: dict) -> DomainApplication:
"""Unpack the form responses onto the model object properties."""
def get_application_object(self) -> DomainApplication:
"""
Attempt to match the current wizard with a DomainApplication.
Will create an application if none exists.
"""
if "application_id" in self.storage.extra_data:
id = self.storage.extra_data["application_id"]
try:
return DomainApplication.objects.get(
creator=self.request.user,
pk=id,
)
except DomainApplication.DoesNotExist:
logger.debug("Application id %s did not have a DomainApplication" % id)
application = DomainApplication.objects.create(creator=self.request.user)
# organization type information
organization_type_data = form_dict["organization_type"].cleaned_data
application.organization_type = organization_type_data["organization_type"]
# federal branch information may not exist
federal_branch_data = form_dict.get("organization_federal")
if federal_branch_data is not None:
federal_branch_data = federal_branch_data.cleaned_data
application.federal_branch = federal_branch_data["federal_type"]
# election board information may not exist.
election_board_data = form_dict.get("organization_election")
if election_board_data is not None:
election_board_data = election_board_data.cleaned_data
application.is_election_office = election_board_data["is_election_board"]
# contact information
contact_data = form_dict["organization_contact"].cleaned_data
application.organization_name = contact_data["organization_name"]
application.street_address = contact_data["address_line1"]
# TODO: add the rest of these fields when they are created in the forms
# This isn't really the requested_domain field
# but we need something in this field to make the form submittable
requested_site, _ = Domain.objects.get_or_create(
name=contact_data["organization_name"] + ".gov"
)
application.requested_domain = requested_site
self.storage.extra_data["application_id"] = application.id
return application
def forms_to_database(
self, forms: dict = None, form: RegistrarForm = None
) -> DomainApplication:
"""
Unpack the form responses onto the model object properties.
Saves the application to the database.
"""
application = self.get_application_object()
if forms:
itr = forms
elif form:
itr = {"form": form}
else:
raise TypeError("forms and form cannot both be None")
for form in itr.values():
if form is not None and hasattr(form, "to_database"):
form.to_database(application)
return application
def process_step(self, form):
"""
Hook called on every POST request, if the form is valid.
Do not manipulate the form data here.
"""
# save progress
self.forms_to_database(form=form)
return self.get_form_step_data(form)
def get_form(self, step=None, data=None, files=None):
"""This method constructs the form for a given step."""
form = super().get_form(step, data, files)
# restore from database, but only if a record has already
# been associated with this wizard instance
if "application_id" in self.storage.extra_data:
application = self.get_application_object()
form.from_database(application)
return form
def post(self, *args, **kwargs):
"""This method handles POST requests."""
step = self.steps.current
# always call super() first, to do important pre-processing
rendered = super().post(*args, **kwargs)
# if user opted to save their progress,
# return them to the page they were already on
button = self.request.POST.get("submit_button", None)
if button == "save":
return self.render_goto_step(step)
# otherwise, proceed as normal
return rendered
def get(self, *args, **kwargs):
"""This method handles GET requests."""
current_url = resolve(self.request.path_info).url_name
# always call super(), it handles important redirect logic
rendered = super().get(*args, **kwargs)
# if user visited via an "edit" url, associate the id of the
# application they are trying to edit to this wizard instance
if current_url == "edit-application" and "id" in kwargs:
self.storage.extra_data["application_id"] = kwargs["id"]
return rendered
def done(self, form_list, form_dict, **kwargs):
application = self.forms_to_object(form_dict)
"""Called when the data for every form is submitted and validated."""
application = self.forms_to_database(forms=form_dict)
application.submit() # change the status to submitted
application.save()
logger.debug("Application object saved: %s", application.id)

View file

@ -0,0 +1,99 @@
# Generated by Django 4.1.3 on 2022-12-02 21:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0002_domain_host_nameserver_hostip_and_more"),
]
operations = [
migrations.RenameField(
model_name="domainapplication",
old_name="is_election_office",
new_name="is_election_board",
),
migrations.RenameField(
model_name="domainapplication",
old_name="acknowledged_policy",
new_name="is_policy_acknowledged",
),
migrations.RenameField(
model_name="domainapplication",
old_name="zip_code",
new_name="zipcode",
),
migrations.RemoveField(
model_name="domainapplication",
name="federal_branch",
),
migrations.RemoveField(
model_name="domainapplication",
name="street_address",
),
migrations.RemoveField(
model_name="domainapplication",
name="unit_number",
),
migrations.RemoveField(
model_name="domainapplication",
name="unit_type",
),
migrations.AddField(
model_name="domainapplication",
name="address_line1",
field=models.TextField(blank=True, help_text="Address line 1", null=True),
),
migrations.AddField(
model_name="domainapplication",
name="address_line2",
field=models.CharField(
blank=True, help_text="Address line 2", max_length=15, null=True
),
),
migrations.AddField(
model_name="domainapplication",
name="federal_type",
field=models.CharField(
blank=True,
choices=[
("executive", "Executive"),
("judicial", "Judicial"),
("legislative", "Legislative"),
],
help_text="Branch of federal government",
max_length=50,
null=True,
),
),
migrations.AlterField(
model_name="domainapplication",
name="organization_type",
field=models.CharField(
blank=True,
choices=[
("federal", "Federal: a federal agency"),
("interstate", "Interstate: an organization of two or more states"),
(
"state_or_territory",
"State or Territory: One of the 50 U.S. states, the District of Columbia, American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. Virgin Islands",
),
(
"tribal",
"Tribal: a tribal government recognized by the federal or state government",
),
("county", "County: a county, parish, or borough"),
("city", "City: a city, town, township, village, etc."),
(
"special_district",
"Special District: an independent organization within a single state",
),
],
help_text="Type of Organization",
max_length=255,
null=True,
),
),
]

View file

@ -1,14 +1,14 @@
import logging
import re
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from epp.mock_epp import domain_info, domain_check
from .utility.time_stamped_model import TimeStampedModel
from .domain_application import DomainApplication
from .user import User
logger = logging.getLogger(__name__)
@ -93,11 +93,58 @@ class Domain(TimeStampedModel):
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
@classmethod
def string_could_be_domain(cls, domain: str) -> bool:
def normalize(cls, domain: str, tld=None) -> str: # noqa: C901
"""Return `domain` in form `<second level>.<tld>`, if possible.
This does not guarantee the returned string is a valid domain name."""
cleaned = domain.lower()
# starts with https or http
if cleaned.startswith("https://"):
cleaned = cleaned[8:]
if cleaned.startswith("http://"):
cleaned = cleaned[7:]
# has url parts
if "/" in cleaned:
cleaned = cleaned.split("/")[0]
# has query parts
if "?" in cleaned:
cleaned = cleaned.split("?")[0]
# has fragments
if "#" in cleaned:
cleaned = cleaned.split("#")[0]
# replace disallowed chars
re.sub(r"^[^A-Za-z0-9.-]+", "", cleaned)
parts = cleaned.split(".")
# has subdomains or invalid repetitions
if cleaned.count(".") > 0:
# remove invalid repetitions
while parts[-1] == parts[-2]:
parts.pop()
# remove subdomains
parts = parts[-2:]
hasTLD = len(parts) == 2
if hasTLD:
# set correct tld
if tld is not None:
parts[-1] = tld
else:
# add tld
if tld is not None:
parts.append(tld)
else:
raise ValueError("You must specify a tld for %s" % domain)
cleaned = ".".join(parts)
return cleaned
@classmethod
def string_could_be_domain(cls, domain: str | None) -> bool:
"""Return True if the string could be a domain name, otherwise False."""
if cls.DOMAIN_REGEX.match(domain):
return True
return False
if not isinstance(domain, str):
return False
return bool(cls.DOMAIN_REGEX.match(domain))
@classmethod
def available(cls, domain: str) -> bool:
@ -137,16 +184,10 @@ class Domain(TimeStampedModel):
# TODO: return an error if registry cannot be contacted
return None
def could_be_domain(self) -> bool:
"""Could this instance be a domain?"""
# short-circuit if self.website is null/None
if not self.name:
return False
return self.string_could_be_domain(str(self.name))
@transition(field="is_active", source="*", target=True)
def activate(self):
"""This domain should be made live."""
DomainApplication = apps.get_model("registrar.DomainApplication")
if hasattr(self, "domain_application"):
if self.domain_application.status != DomainApplication.APPROVED:
raise ValueError("Cannot activate. Application must be approved.")
@ -166,6 +207,34 @@ class Domain(TimeStampedModel):
# if there is a feature request to implement this
raise Exception("Cannot revoke, contact registry.")
@property
def sld(self):
"""Get or set the second level domain string."""
return self.name.split(".")[0]
@sld.setter
def sld(self, value: str):
parts = self.name.split(".")
tld = parts[1] if len(parts) > 1 else ""
if Domain.string_could_be_domain(f"{value}.{tld}"):
self.name = f"{value}.{tld}"
else:
raise ValidationError("%s is not a valid second level domain" % value)
@property
def tld(self):
"""Get or set the top level domain string."""
parts = self.name.split(".")
return parts[1] if len(parts) > 1 else ""
@tld.setter
def tld(self, value: str):
sld = self.name.split(".")[0]
if Domain.string_could_be_domain(f"{sld}.{value}"):
self.name = f"{sld}.{value}"
else:
raise ValidationError("%s is not a valid top level domain" % value)
def __str__(self) -> str:
return self.name
@ -232,6 +301,6 @@ class Domain(TimeStampedModel):
# TODO: determine the relationship between this field
# and the domain application's `creator` and `submitter`
owners = models.ManyToManyField(
User,
"registrar.User",
help_text="",
)

View file

@ -1,14 +1,11 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Union
from django.apps import apps
from django.db import models
from django_fsm import FSMField, transition # type: ignore
from .utility.time_stamped_model import TimeStampedModel
from .contact import Contact
from .user import User
from .website import Website
from typing import TYPE_CHECKING, Union
if TYPE_CHECKING:
from ..forms.application_wizard import ApplicationWizard
@ -30,35 +27,86 @@ class DomainApplication(TimeStampedModel):
(APPROVED, APPROVED),
]
FEDERAL = "federal"
INTERSTATE = "interstate"
STATE_OR_TERRITORY = "state_or_territory"
TRIBAL = "tribal"
COUNTY = "county"
CITY = "city"
SPECIAL_DISTRICT = "special_district"
ORGANIZATION_CHOICES = [
(FEDERAL, "a federal agency"),
(INTERSTATE, "an organization of two or more states"),
(
STATE_OR_TERRITORY,
"one of the 50 U.S. states, the District of "
"Columbia, American Samoa, Guam, Northern Mariana Islands, "
"Puerto Rico, or the U.S. Virgin Islands",
),
(
TRIBAL,
"a tribal government recognized by the federal or " "state government",
),
(COUNTY, "a county, parish, or borough"),
(CITY, "a city, town, township, village, etc."),
(SPECIAL_DISTRICT, "an independent organization within a single state"),
]
class StateTerritoryChoices(models.TextChoices):
ALABAMA = "AL", "Alabama"
ALASKA = "AK", "Alaska"
ARIZONA = "AZ", "Arizona"
ARKANSAS = "AR", "Arkansas"
CALIFORNIA = "CA", "California"
COLORADO = "CO", "Colorado"
CONNECTICUT = "CT", "Connecticut"
DELAWARE = "DE", "Delaware"
DISTRICT_OF_COLUMBIA = "DC", "District of Columbia"
FLORIDA = "FL", "Florida"
GEORGIA = "GA", "Georgia"
HAWAII = "HI", "Hawaii"
IDAHO = "ID", "Idaho"
ILLINOIS = "IL", "Illinois"
INDIANA = "IN", "Indiana"
IOWA = "IA", "Iowa"
KANSAS = "KS", "Kansas"
KENTUCKY = "KY", "Kentucky"
LOUISIANA = "LA", "Louisiana"
MAINE = "ME", "Maine"
MARYLAND = "MD", "Maryland"
MASSACHUSETTS = "MA", "Massachusetts"
MICHIGAN = "MI", "Michigan"
MINNESOTA = "MN", "Minnesota"
MISSISSIPPI = "MS", "Mississippi"
MISSOURI = "MO", "Missouri"
MONTANA = "MT", "Montana"
NEBRASKA = "NE", "Nebraska"
NEVADA = "NV", "Nevada"
NEW_HAMPSHIRE = "NH", "New Hampshire"
NEW_JERSEY = "NJ", "New Jersey"
NEW_MEXICO = "NM", "New Mexico"
NEW_YORK = "NY", "New York"
NORTH_CAROLINA = "NC", "North Carolina"
NORTH_DAKOTA = "ND", "North Dakota"
OHIO = "OH", "Ohio"
OKLAHOMA = "OK", "Oklahoma"
OREGON = "OR", "Oregon"
PENNSYLVANIA = "PA", "Pennsylvania"
RHODE_ISLAND = "RI", "Rhode Island"
SOUTH_CAROLINA = "SC", "South Carolina"
SOUTH_DAKOTA = "SD", "South Dakota"
TENNESSEE = "TN", "Tennessee"
TEXAS = "TX", "Texas"
UTAH = "UT", "Utah"
VERMONT = "VT", "Vermont"
VIRGINIA = "VA", "Virginia"
WASHINGTON = "WA", "Washington"
WEST_VIRGINIA = "WV", "West Virginia"
WISCONSIN = "WI", "Wisconsin"
WYOMING = "WY", "Wyoming"
AMERICAN_SAMOA = "AS", "American Samoa"
GUAM = "GU", "Guam"
NORTHERN_MARIANA_ISLANDS = "MP", "Northern Mariana Islands"
PUERTO_RICO = "PR", "Puerto Rico"
VIRGIN_ISLANDS = "VI", "Virgin Islands"
EXECUTIVE = "Executive"
JUDICIAL = "Judicial"
LEGISLATIVE = "Legislative"
BRANCH_CHOICES = [(x, x) for x in (EXECUTIVE, JUDICIAL, LEGISLATIVE)]
class OrganizationChoices(models.TextChoices):
FEDERAL = "federal", "Federal: a federal agency"
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
STATE_OR_TERRITORY = "state_or_territory", (
"State or Territory: One of the 50 U.S. states, the District of "
"Columbia, American Samoa, Guam, Northern Mariana Islands, "
"Puerto Rico, or the U.S. Virgin Islands"
)
TRIBAL = "tribal", (
"Tribal: a tribal government recognized by the federal or "
"state government"
)
COUNTY = "county", "County: a county, parish, or borough"
CITY = "city", "City: a city, town, township, village, etc."
SPECIAL_DISTRICT = "special_district", (
"Special District: an independent organization within a single state"
)
class BranchChoices(models.TextChoices):
EXECUTIVE = "executive", "Executive"
JUDICIAL = "judicial", "Judicial"
LEGISLATIVE = "legislative", "Legislative"
# #### Internal fields about the application #####
status = FSMField(
@ -69,10 +117,12 @@ class DomainApplication(TimeStampedModel):
# This is the application user who created this application. The contact
# information that they gave is in the `submitter` field
creator = models.ForeignKey(
User, on_delete=models.PROTECT, related_name="applications_created"
"registrar.User",
on_delete=models.PROTECT,
related_name="applications_created",
)
investigator = models.ForeignKey(
User,
"registrar.User",
null=True,
blank=True,
on_delete=models.SET_NULL,
@ -82,21 +132,21 @@ class DomainApplication(TimeStampedModel):
# ##### data fields from the initial form #####
organization_type = models.CharField(
max_length=255,
choices=ORGANIZATION_CHOICES,
choices=OrganizationChoices.choices,
null=True,
blank=True,
help_text="Type of Organization",
)
federal_branch = models.CharField(
federal_type = models.CharField(
max_length=50,
choices=BRANCH_CHOICES,
choices=BranchChoices.choices,
null=True,
blank=True,
help_text="Branch of federal government",
)
is_election_office = models.BooleanField(
is_election_board = models.BooleanField(
null=True,
blank=True,
help_text="Is your ogranization an election office?",
@ -108,22 +158,16 @@ class DomainApplication(TimeStampedModel):
help_text="Organization name",
db_index=True,
)
street_address = models.TextField(
address_line1 = models.TextField(
null=True,
blank=True,
help_text="Street Address",
help_text="Address line 1",
)
unit_type = models.CharField(
address_line2 = models.CharField(
max_length=15,
null=True,
blank=True,
help_text="Unit type",
)
unit_number = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Unit number",
help_text="Address line 2",
)
state_territory = models.CharField(
max_length=2,
@ -131,7 +175,7 @@ class DomainApplication(TimeStampedModel):
blank=True,
help_text="State/Territory",
)
zip_code = models.CharField(
zipcode = models.CharField(
max_length=10,
null=True,
blank=True,
@ -140,7 +184,7 @@ class DomainApplication(TimeStampedModel):
)
authorizing_official = models.ForeignKey(
Contact,
"registrar.Contact",
null=True,
blank=True,
related_name="authorizing_official",
@ -149,7 +193,7 @@ class DomainApplication(TimeStampedModel):
# "+" means no reverse relation to lookup applications from Website
current_websites = models.ManyToManyField(
Website,
"registrar.Website",
blank=True,
related_name="current+",
)
@ -163,7 +207,7 @@ class DomainApplication(TimeStampedModel):
on_delete=models.PROTECT,
)
alternative_domains = models.ManyToManyField(
Website,
"registrar.Website",
blank=True,
related_name="alternatives+",
)
@ -171,7 +215,7 @@ class DomainApplication(TimeStampedModel):
# This is the contact information provided by the applicant. The
# application user who created it is in the `creator` field.
submitter = models.ForeignKey(
Contact,
"registrar.Contact",
null=True,
blank=True,
related_name="submitted_applications",
@ -185,7 +229,7 @@ class DomainApplication(TimeStampedModel):
)
other_contacts = models.ManyToManyField(
Contact,
"registrar.Contact",
blank=True,
related_name="contact_applications",
)
@ -203,7 +247,7 @@ class DomainApplication(TimeStampedModel):
help_text="Anything else we should know?",
)
acknowledged_policy = models.BooleanField(
is_policy_acknowledged = models.BooleanField(
null=True,
blank=True,
help_text="Acknowledged .gov acceptable use policy",
@ -226,8 +270,15 @@ class DomainApplication(TimeStampedModel):
# can raise more informative exceptions
# requested_domain could be None here
if (not self.requested_domain) or (not self.requested_domain.could_be_domain()):
raise ValueError("Requested domain is not a legal domain name.")
if not hasattr(self, "requested_domain"):
raise ValueError("Requested domain is missing.")
if self.requested_domain is None:
raise ValueError("Requested domain is missing.")
Domain = apps.get_model("registrar.Domain")
if not Domain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.")
# if no exception was raised, then we don't need to do anything
# inside this method, keep the `pass` here to remind us of that
@ -253,7 +304,8 @@ class DomainApplication(TimeStampedModel):
@staticmethod
def show_organization_federal(wizard: ApplicationWizard) -> bool:
"""Show this step if the answer to the first question was "federal"."""
return DomainApplication._get_organization_type(wizard) == "Federal"
user_choice = DomainApplication._get_organization_type(wizard)
return user_choice == DomainApplication.OrganizationChoices.FEDERAL
@staticmethod
def show_organization_election(wizard: ApplicationWizard) -> bool:
@ -261,7 +313,9 @@ class DomainApplication(TimeStampedModel):
This shows for answers that aren't "Federal" or "Interstate".
"""
type_answer = DomainApplication._get_organization_type(wizard)
if type_answer and type_answer not in ("Federal", "Interstate"):
return True
return False
user_choice = DomainApplication._get_organization_type(wizard)
excluded = [
DomainApplication.OrganizationChoices.FEDERAL,
DomainApplication.OrganizationChoices.INTERSTATE,
]
return bool(user_choice and user_choice not in excluded)

View file

@ -1,7 +1,6 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
from .domain import Domain
class Host(TimeStampedModel):
@ -26,7 +25,7 @@ class Host(TimeStampedModel):
)
domain = models.ForeignKey(
Domain,
"registrar.Domain",
on_delete=models.PROTECT,
related_name="host", # access this Host via the Domain as `domain.host`
help_text="Domain to which this host belongs",

View file

@ -2,7 +2,6 @@ from django.db import models
from django.core.validators import validate_ipv46_address
from .utility.time_stamped_model import TimeStampedModel
from .host import Host
class HostIP(TimeStampedModel):
@ -25,7 +24,7 @@ class HostIP(TimeStampedModel):
)
host = models.ForeignKey(
Host,
"registrar.Host",
on_delete=models.PROTECT,
related_name="ip", # access this HostIP via the Host as `host.ip`
help_text="Host to which this IP address belongs",

View file

@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel
from .utility.address_model import AddressModel
from .contact import Contact
from .user import User
class UserProfile(TimeStampedModel, Contact, AddressModel):
@ -12,7 +11,7 @@ class UserProfile(TimeStampedModel, Contact, AddressModel):
"""User information, unrelated to their login/auth details."""
user = models.OneToOneField(
User,
"registrar.User",
null=True,
blank=True,
on_delete=models.CASCADE,

View file

@ -1,3 +1,5 @@
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import models
@ -14,5 +16,35 @@ class Website(models.Model):
help_text="",
)
@property
def sld(self):
"""Get or set the second level domain string."""
return self.website.split(".")[0]
@sld.setter
def sld(self, value: str):
Domain = apps.get_model("registrar.Domain")
parts = self.website.split(".")
tld = parts[1] if len(parts) > 1 else ""
if Domain.string_could_be_domain(f"{value}.{tld}"):
self.website = f"{value}.{tld}"
else:
raise ValidationError("%s is not a valid second level domain" % value)
@property
def tld(self):
"""Get or set the top level domain string."""
parts = self.website.split(".")
return parts[1] if len(parts) > 1 else ""
@tld.setter
def tld(self, value: str):
Domain = apps.get_model("registrar.Domain")
sld = self.website.split(".")[0]
if Domain.string_could_be_domain(f"{sld}.{value}"):
self.website = f"{sld}.{value}"
else:
raise ValidationError("%s is not a valid top level domain" % value)
def __str__(self) -> str:
return str(self.website)

View file

@ -27,10 +27,10 @@
{{ wizard.management_form }}
{% csrf_token %}
{{ wizard.form.dotgov_domain|add_label_class:"usa-label" }}
{{ wizard.form.requested_domain|add_label_class:"usa-label" }}
<div class="display-flex flex-align-center">
<span class="padding-top-05 padding-right-2px">www.</span>
{{ wizard.form.dotgov_domain|add_class:"usa-input"|attr:"aria-describedby:domain_instructions" }}
{{ wizard.form.requested_domain|add_class:"usa-input"|attr:"aria-describedby:domain_instructions" }}
<span class="padding-top-05 padding-left-2px">.gov </span>
</div>
<button type="button" class="usa-button">Check availability </button>

View file

@ -20,11 +20,11 @@
<h1> {{form_titles|get_item:wizard.steps.current}} </h1>
{% block form_content %}
{% if wizard.steps.next %}
<button type="submit" class="usa-button">Next</button>
<button type="submit" name="submit_button" value="next" class="usa-button">Next</button>
{% else %}
<button type="submit" class="usa-button">Submit your domain request</button>
{% endif %}
<button type="button" class="usa-button usa-button--outline">Save</button>
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--outline">Save</button>
</main>
</div>
{% endblock %}

View file

@ -28,9 +28,9 @@
{{ wizard.form.address_line1|add_class:"usa-input" }}
{{ wizard.form.address_line2|add_label_class:"usa-label" }}
{{ wizard.form.address_line2|add_class:"usa-input" }}
{{ wizard.form.us_state|add_label_class:"usa-label" }}
<div class="usa-combo-box">
{{ wizard.form.us_state|add_class:"usa-select" }}
{{ wizard.form.state_territory|add_label_class:"usa-label" }}
<div class="usa-combo-box" data-default-value>
{{ wizard.form.state_territory|add_class:"usa-select" }}
</div>
{{ wizard.form.zipcode|add_label_class:"usa-label" }}
{{ wizard.form.zipcode|add_class:"usa-input usa-input--small" }}

View file

@ -13,8 +13,9 @@
<h2>Is your organization an election office?</h2>
</legend>
{% radio_buttons_by_value wizard.form.is_election_board as choices %}
{% include "includes/radio_button.html" with choice=choices|get_item:True %}
{% include "includes/radio_button.html" with choice=choices|get_item:False %}
{% for choice in choices.values %}
{% include "includes/radio_button.html" with choice=choice tile="true" %}
{% endfor %}
</fieldset>
{{ block.super }}

View file

@ -12,10 +12,10 @@
<legend>
<h2>Which federal branch is your organization in?</h2>
</legend>
{% radio_buttons_by_value wizard.form.federal_type as federal_choices %}
{% include "includes/radio_button.html" with choice=federal_choices.Executive%}
{% include "includes/radio_button.html" with choice=federal_choices.Judicial%}
{% include "includes/radio_button.html" with choice=federal_choices.Legislative%}
{% radio_buttons_by_value wizard.form.federal_type as choices %}
{% for choice in choices.values %}
{% include "includes/radio_button.html" with choice=choice tile="true" %}
{% endfor %}
</fieldset>
{{ block.super }}

View file

@ -16,13 +16,9 @@
<h2> What kind of government organization do you represent?</h2>
</legend>
{{ wizard.form.organization_type.errors }}
{% include "includes/radio_button.html" with choice=choices.Federal tile="true" %}
{% include "includes/radio_button.html" with choice=choices.Interstate tile="true" %}
{% include "includes/radio_button.html" with choice=choices.State_or_Territory tile="true" %}
{% include "includes/radio_button.html" with choice=choices.Tribal tile="true" %}
{% include "includes/radio_button.html" with choice=choices.County tile="true" %}
{% include "includes/radio_button.html" with choice=choices.City tile="true" %}
{% include "includes/radio_button.html" with choice=choices.Special_District tile="true" %}
{% for choice in choices.values %}
{% include "includes/radio_button.html" with choice=choice tile="true" %}
{% endfor %}
</fieldset>
{{ block.super }}

View file

@ -12,8 +12,8 @@
{% csrf_token %}
<div class="usa-character-count">
{{ wizard.form.purpose_field|add_label_class:"usa-label usa-sr-only" }}
{{ wizard.form.purpose_field|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }}
{{ wizard.form.purpose|add_label_class:"usa-label usa-sr-only" }}
{{ wizard.form.purpose|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }}
<span class="usa-character-count__message" id="with-hint-textarea-info with-hint-textarea-hint"> You can enter up to 500 characters </span>
</div>
</div>

View file

@ -60,8 +60,8 @@
{% csrf_token %}
<div class="usa-checkbox">
{{ wizard.form.agree_check|add_class:"usa-checkbox__input"}}
{{ wizard.form.agree_check|add_label_class:"usa-checkbox__label" }}
{{ wizard.form.is_policy_acknowledged|add_class:"usa-checkbox__input"}}
{{ wizard.form.is_policy_acknowledged|add_label_class:"usa-checkbox__label" }}
</div>
</div>

View file

@ -11,8 +11,8 @@
{{ wizard.management_form }}
{% csrf_token %}
{{ wizard.form.email|add_label_class:"usa-label" }}
{{ wizard.form.email|add_class:"usa-input"|attr:"aria-describedby:instructions" }}
{{ wizard.form.security_email|add_label_class:"usa-label" }}
{{ wizard.form.security_email|add_class:"usa-input"|attr:"aria-describedby:instructions" }}
{{ block.super }}

View file

@ -27,22 +27,21 @@ class TestDomainApplication(TestCase):
application = DomainApplication.objects.create(
creator=user,
investigator=user,
organization_type=DomainApplication.FEDERAL,
federal_branch=DomainApplication.EXECUTIVE,
is_election_office=False,
organization_type=DomainApplication.OrganizationChoices.FEDERAL,
federal_type=DomainApplication.BranchChoices.EXECUTIVE,
is_election_board=False,
organization_name="Test",
street_address="100 Main St.",
unit_type="APT",
unit_number="1A",
address_line1="100 Main St.",
address_line2="APT 1A",
state_territory="CA",
zip_code="12345-6789",
zipcode="12345-6789",
authorizing_official=contact,
requested_domain=domain,
submitter=contact,
purpose="Igorville rules!",
security_email="security@igorville.gov",
anything_else="All of Igorville loves the dotgov program.",
acknowledged_policy=True,
is_policy_acknowledged=True,
)
application.current_websites.add(com_website)
application.alternative_domains.add(gov_website)

View file

@ -1,3 +1,5 @@
from unittest import skip
from django.conf import settings
from django.test import Client, TestCase
from django.urls import reverse
@ -5,7 +7,7 @@ from django.contrib.auth import get_user_model
from django_webtest import WebTest # type: ignore
from registrar.models import DomainApplication, Domain
from registrar.models import DomainApplication, Domain, Contact, Website
from registrar.forms.application_wizard import TITLES
from .common import less_console_noise
@ -87,9 +89,9 @@ class LoggedInTests(TestWithUser):
)
class FormTests(TestWithUser, WebTest):
class DomainApplicationTests(TestWithUser, WebTest):
"""Webtests for forms to test filling and submitting."""
"""Webtests for domain application to test filling and submitting."""
# Doesn't work with CSRF checking
# hypothesis is that CSRF_USE_SESSIONS is incompatible with WebTest
@ -100,7 +102,7 @@ class FormTests(TestWithUser, WebTest):
self.app.set_user(self.user.username)
def tearDown(self):
# delete any applications we made so that users can be deleted\
# delete any applications we made so that users can be deleted
DomainApplication.objects.all().delete()
super().tearDown()
@ -116,6 +118,10 @@ class FormTests(TestWithUser, WebTest):
As we add additional form pages, we need to include them here to make
this test work.
"""
num_pages_tested = 0
SKIPPED_PAGES = 1 # elections
num_pages = len(TITLES) - SKIPPED_PAGES
type_page = self.app.get(reverse("application")).follow()
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
@ -125,9 +131,18 @@ class FormTests(TestWithUser, WebTest):
# ---- TYPE PAGE ----
type_form = type_page.form
type_form["organization_type-organization_type"] = "Federal"
type_form["organization_type-organization_type"] = "federal"
# set the session ID before .submit()
# test saving the page
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/")
# 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()
@ -135,20 +150,31 @@ class FormTests(TestWithUser, WebTest):
# the application
self.assertEquals(type_result.status_code, 302)
self.assertEquals(type_result["Location"], "/register/organization_federal/")
num_pages_tested += 1
# ---- FEDERAL BRANCH PAGE ----
# Follow the redirect to the next form page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
federal_page = type_result.follow()
federal_form = federal_page.form
federal_form["organization_federal-federal_type"] = "Executive"
federal_form["organization_federal-federal_type"] = "executive"
# set the session ID before .submit()
# 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()
self.assertEquals(federal_result.status_code, 302)
self.assertEquals(federal_result["Location"], "/register/organization_contact/")
num_pages_tested += 1
# ---- ORG CONTACT PAGE ----
# Follow the redirect to the next form page
@ -156,9 +182,22 @@ class FormTests(TestWithUser, WebTest):
org_contact_form = org_contact_page.form
org_contact_form["organization_contact-organization_name"] = "Testorg"
org_contact_form["organization_contact-address_line1"] = "address 1"
org_contact_form["organization_contact-us_state"] = "NY"
org_contact_form["organization_contact-state_territory"] = "NY"
org_contact_form["organization_contact-zipcode"] = "10002"
# test saving the page
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
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.organization_name, "Testorg")
self.assertEquals(application.address_line1, "address 1")
self.assertEquals(application.state_territory, "NY")
self.assertEquals(application.zipcode, "10002")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
org_contact_result = org_contact_form.submit()
@ -166,6 +205,8 @@ class FormTests(TestWithUser, WebTest):
self.assertEquals(
org_contact_result["Location"], "/register/authorizing_official/"
)
num_pages_tested += 1
# ---- AUTHORIZING OFFICIAL PAGE ----
# Follow the redirect to the next form page
ao_page = org_contact_result.follow()
@ -176,11 +217,26 @@ class FormTests(TestWithUser, WebTest):
ao_form["authorizing_official-email"] = "testy@town.com"
ao_form["authorizing_official-phone"] = "(555) 555 5555"
# test saving the page
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
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.authorizing_official.first_name, "Testy")
self.assertEquals(application.authorizing_official.last_name, "Tester")
self.assertEquals(application.authorizing_official.title, "Chief Tester")
self.assertEquals(application.authorizing_official.email, "testy@town.com")
self.assertEquals(application.authorizing_official.phone, "(555) 555 5555")
# test next button
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["Location"], "/register/current_sites/")
num_pages_tested += 1
# ---- CURRENT SITES PAGE ----
# Follow the redirect to the next form page
@ -188,35 +244,73 @@ class FormTests(TestWithUser, WebTest):
current_sites_form = current_sites_page.form
current_sites_form["current_sites-current_site"] = "www.city.com"
# test saving the page
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
application = DomainApplication.objects.get() # there's only one
self.assertEquals(
application.current_websites.filter(website="city.com").count(), 1
)
# test next button
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["Location"], "/register/dotgov_domain/")
num_pages_tested += 1
# ---- DOTGOV DOMAIN PAGE ----
# Follow the redirect to the next form page
dotgov_page = current_sites_result.follow()
dotgov_form = dotgov_page.form
dotgov_form["dotgov_domain-dotgov_domain"] = "city"
dotgov_form["dotgov_domain-requested_domain"] = "city"
dotgov_form["dotgov_domain-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
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()
self.assertEquals(dotgov_result.status_code, 302)
self.assertEquals(dotgov_result["Location"], "/register/purpose/")
num_pages_tested += 1
# ---- PURPOSE DOMAIN PAGE ----
# ---- PURPOSE PAGE ----
# Follow the redirect to the next form page
purpose_page = dotgov_result.follow()
purpose_form = purpose_page.form
purpose_form["purpose-purpose_field"] = "Purpose of the site"
purpose_form["purpose-purpose"] = "Purpose of the site"
# 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, "Purpose of the site")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
purpose_result = purpose_form.submit()
self.assertEquals(purpose_result.status_code, 302)
self.assertEquals(purpose_result["Location"], "/register/your_contact/")
num_pages_tested += 1
# ---- YOUR CONTACT INFO PAGE ----
# Follow the redirect to the next form page
@ -229,11 +323,26 @@ class FormTests(TestWithUser, WebTest):
your_contact_form["your_contact-email"] = "testy-admin@town.com"
your_contact_form["your_contact-phone"] = "(555) 555 5556"
# test saving the page
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
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, "(555) 555 5556")
# test next button
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["Location"], "/register/other_contacts/")
num_pages_tested += 1
# ---- OTHER CONTACTS PAGE ----
# Follow the redirect to the next form page
@ -246,6 +355,25 @@ class FormTests(TestWithUser, WebTest):
other_contacts_form["other_contacts-email"] = "testy2@town.com"
other_contacts_form["other_contacts-phone"] = "(555) 555 5557"
# test saving the page
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
application = DomainApplication.objects.get() # there's only one
self.assertEquals(
application.other_contacts.filter(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
).count(),
1,
)
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
other_contacts_result = other_contacts_form.submit()
@ -253,19 +381,31 @@ class FormTests(TestWithUser, WebTest):
self.assertEquals(
other_contacts_result["Location"], "/register/security_email/"
)
num_pages_tested += 1
# ---- SECURITY EMAIL PAGE ----
# Follow the redirect to the next form page
security_email_page = other_contacts_result.follow()
security_email_form = security_email_page.form
security_email_form["security_email-email"] = "security@city.com"
security_email_form["security_email-security_email"] = "security@city.com"
# test saving the page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
result = security_email_page.form.submit("submit_button", value="save")
# should remain on the same page
self.assertEquals(result["Location"], "/register/security_email/")
# should see results in db
application = DomainApplication.objects.get() # there's only one
self.assertEquals(application.security_email, "security@city.com")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
security_email_result = security_email_form.submit()
self.assertEquals(security_email_result.status_code, 302)
self.assertEquals(security_email_result["Location"], "/register/anything_else/")
num_pages_tested += 1
# ---- ANYTHING ELSE PAGE ----
# Follow the redirect to the next form page
@ -274,37 +414,65 @@ class FormTests(TestWithUser, WebTest):
anything_else_form["anything_else-anything_else"] = "No"
# 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, "No")
# test next button
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
anything_else_result = anything_else_form.submit()
self.assertEquals(anything_else_result.status_code, 302)
self.assertEquals(anything_else_result["Location"], "/register/requirements/")
num_pages_tested += 1
# ---- REQUIREMENTS PAGE ----
# Follow the redirect to the next form page
requirements_page = anything_else_result.follow()
requirements_form = requirements_page.form
requirements_form["requirements-agree_check"] = 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
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
requirements_result = requirements_form.submit()
self.assertEquals(requirements_result.status_code, 302)
self.assertEquals(requirements_result["Location"], "/register/review/")
num_pages_tested += 1
# ---- REVIEW AND FINSIHED PAGES ----
# Follow the redirect to the next form page
review_page = requirements_result.follow()
review_form = review_page.form
# final submission results in a redirect to the "finished" URL
# 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)
review_result = review_form.submit()
self.assertEquals(review_result.status_code, 302)
self.assertEquals(review_result["Location"], "/register/finished/")
num_pages_tested += 1
# following this redirect is a GET request, so include the cookie
# here too.
@ -313,6 +481,9 @@ class FormTests(TestWithUser, WebTest):
final_result = review_result.follow()
self.assertContains(final_result, "Thank you for your domain request")
# check that any new pages are added to this test
self.assertEqual(num_pages, num_pages_tested)
def test_application_form_conditional_federal(self):
"""Federal branch question is shown for federal organizations."""
type_page = self.app.get(reverse("application")).follow()
@ -328,7 +499,7 @@ class FormTests(TestWithUser, WebTest):
self.assertNotContains(type_page, TITLES["organization_federal"])
self.assertNotContains(type_page, TITLES["organization_election"])
type_form = type_page.form
type_form["organization_type-organization_type"] = "Federal"
type_form["organization_type-organization_type"] = "federal"
# set the session ID before .submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -361,14 +532,13 @@ class FormTests(TestWithUser, WebTest):
self.assertNotContains(type_page, TITLES["organization_federal"])
self.assertNotContains(type_page, TITLES["organization_election"])
type_form = type_page.form
type_form["organization_type-organization_type"] = "County"
type_form["organization_type-organization_type"] = "county"
# set the session ID before .submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
type_result = type_form.submit()
# the post request should return a redirect to the federal branch
# question
# the post request should return a redirect to the elections question
self.assertEquals(type_result.status_code, 302)
self.assertEquals(type_result["Location"], "/register/organization_election/")
@ -378,3 +548,124 @@ class FormTests(TestWithUser, WebTest):
election_page = type_result.follow()
self.assertContains(election_page, TITLES["organization_election"])
self.assertNotContains(election_page, TITLES["organization_federal"])
@skip("WIP")
def test_application_edit_restore(self):
"""
Test that a previously saved application is available at the /edit endpoint.
"""
ao, _ = Contact.objects.get_or_create(
first_name="Testy",
last_name="Tester",
title="Chief Tester",
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = Domain.objects.get_or_create(name="city.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(
first_name="Testy you",
last_name="Tester you",
title="Admin Tester",
email="testy-admin@town.com",
phone="(555) 555 5556",
)
other, _ = Contact.objects.get_or_create(
first_name="Testy2",
last_name="Tester2",
title="Another Tester",
email="testy2@town.com",
phone="(555) 555 5557",
)
application, _ = DomainApplication.objects.get_or_create(
organization_type="federal",
federal_type="executive",
purpose="Purpose of the site",
security_email="security@city.com",
anything_else="No",
is_policy_acknowledged=True,
organization_name="Testorg",
address_line1="address 1",
state_territory="NY",
zipcode="10002",
authorizing_official=ao,
requested_domain=domain,
submitter=you,
creator=self.user,
)
application.other_contacts.add(other)
application.current_websites.add(current)
application.alternative_domains.add(alt)
# prime the form by visiting /edit
url = reverse("edit-application", kwargs={"id": application.pk})
response = self.client.get(url)
url = reverse("application_step", kwargs={"step": "organization_type"})
response = self.client.get(url, follow=True)
self.assertContains(response, "<input>")
# choices = response.context['wizard']['form']['organization_type'].subwidgets
# radio = [ x for x in choices if x.data["value"] == "federal" ][0]
# checked = radio.data["selected"]
# self.assertTrue(checked)
# url = reverse("application_step", kwargs={"step": "organization_federal"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "organization_contact"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "authorizing_official"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "current_sites"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "dotgov_domain"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "purpose"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "your_contact"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "other_contacts"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "other_contacts"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "security_email"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "anything_else"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
# url = reverse("application_step", kwargs={"step": "requirements"})
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")