Merge branch 'main' into nmb/federal-combobox

This commit is contained in:
Neil Martinsen-Burrell 2022-12-07 09:26:08 -06:00
commit 5d56c88ba5
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
21 changed files with 1021 additions and 319 deletions

View file

@ -8,63 +8,53 @@ from typing import Union
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=[
(DomainApplication.FEDERAL, "Federal: a federal agency"),
(
DomainApplication.INTERSTATE,
"Interstate: an organization of two or more states",
),
(
DomainApplication.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"
),
),
(
DomainApplication.TRIBAL,
(
"Tribal: a tribal government recognized by the federal or "
"state government"
),
),
(DomainApplication.COUNTY, "County: a county, parish, or borough"),
(DomainApplication.CITY, "City: a city, town, township, village, etc."),
(
DomainApplication.SPECIAL_DISTRICT,
"Special District: an independent organization within a single state",
),
],
choices=DomainApplication.OrganizationChoices.choices,
widget=forms.RadioSelect,
)
class OrganizationFederalForm(RegistrarForm):
federal_type = forms.ChoiceField(
required=False,
choices=DomainApplication.BRANCH_CHOICES,
choices=DomainApplication.BranchChoices.choices,
widget=forms.RadioSelect,
)
@ -77,7 +67,6 @@ class OrganizationElectionForm(RegistrarForm):
(False, "No"),
],
),
required=False,
)
@ -236,71 +225,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,
@ -313,6 +263,22 @@ 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"], blank=True)
if normalized:
# 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, "
@ -321,7 +287,42 @@ 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", blank=True
)
if normalized:
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", blank=True
)
if normalized:
# 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 "
@ -330,10 +331,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,
@ -346,6 +366,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,
@ -358,7 +399,7 @@ class OtherContactsForm(RegistrarForm):
class SecurityEmailForm(RegistrarForm):
email = forms.EmailField(
security_email = forms.EmailField(
required=False,
label="Security email",
)
@ -366,77 +407,104 @@ class SecurityEmailForm(RegistrarForm):
class AnythingElseForm(RegistrarForm):
anything_else = forms.CharField(
required=False, label="Anything else we should know", widget=forms.Textarea()
required=False,
label="Anything else we should know",
widget=forms.Textarea(),
)
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",
}
@ -456,6 +524,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
@ -495,42 +567,97 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
context["is_federal"] = self._is_federal()
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)