mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-09 22:14:43 +02:00
Merge branch 'main' into nmb/field-validation
This commit is contained in:
commit
8d3991614e
42 changed files with 925 additions and 532 deletions
2
.github/workflows/loaddata.yaml
vendored
2
.github/workflows/loaddata.yaml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
||||
cf_org: cisa-getgov-prototyping
|
||||
cf_space: staging
|
||||
full_command: "cf run-task getgov-staging --wait --command 'python manage.py flush' --name flush"
|
||||
full_command: "cf run-task getgov-staging --wait --command 'python manage.py flush --no-input' --name flush"
|
||||
|
||||
- name: Load fake data for staging
|
||||
uses: 18f/cg-deploy-action@main
|
||||
|
|
29
docs/architecture/decisions/0016-django-form-wizard.md
Normal file
29
docs/architecture/decisions/0016-django-form-wizard.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 16. Django Form Wizard
|
||||
|
||||
Date: 2022-01-03
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The application form by which registrants apply for a .gov domain is presented over many pages.
|
||||
|
||||
Because we use server-side rendering, each page of the application is a unique HTML page with form fields surrounded by a form tag.
|
||||
|
||||
Needing a way to coordinate state between the pages as a user fills in their application, we initially used the Form wizard from [django-formtools](https://django-formtools.readthedocs.io/en/latest/wizard.html). This eventually proved unworkable due to the lack of native ability to have more than one Django form object displayed on a single HTML page.
|
||||
|
||||
However, a significant portion of the user workflow had already been coded, so it seemed prudent to port some of the formtools logic into our codebase.
|
||||
|
||||
## Decision
|
||||
|
||||
To maintain each page of the domain application as its own Django view class, inheriting common code from a parent class.
|
||||
|
||||
To maintain Django form and formset class in accordance with the Django models whose data they collect, independently of the pages on which they appear.
|
||||
|
||||
## Consequences
|
||||
|
||||
The wizard implementation is now unique to our codebase, which will impact developer onboarding, in the form of additional time needed to understand how it works.
|
||||
|
||||
A small amount of additional code to maintain is introduced. Impact is likely to be minor. Library functions which were not needed by our implementation were not ported.
|
|
@ -2,5 +2,6 @@
|
|||
max-line-length = 88
|
||||
max-complexity = 10
|
||||
extend-ignore = E203
|
||||
per-file-ignores = __init__.py:F401,F403
|
||||
# migrations are auto-generated and often break rules
|
||||
exclude=registrar/migrations/*
|
||||
|
|
|
@ -17,7 +17,6 @@ oic = "*"
|
|||
pyjwkest = "*"
|
||||
psycopg2-binary = "*"
|
||||
whitenoise = "*"
|
||||
django-formtools = "*"
|
||||
django-widget-tweaks = "*"
|
||||
cachetools = "*"
|
||||
requests = "*"
|
||||
|
|
64
src/Pipfile.lock
generated
64
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c9762342448f1a70dbe93cc496e5fb868c67a73f3e51c4440e726f492f7dc5ee"
|
||||
"sha256": "1668475ce39851bd84ff7be330afe9766f6823cf9095980ba3b220ced3a284f4"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
|
@ -120,7 +120,7 @@
|
|||
"sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845",
|
||||
"sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"version": "==2.1.1"
|
||||
},
|
||||
"cryptography": {
|
||||
|
@ -184,11 +184,11 @@
|
|||
},
|
||||
"django-allow-cidr": {
|
||||
"hashes": [
|
||||
"sha256:2fd88ffe697caf0c1d0fd147b88cf44d81282c069bbc475166a2ff1637ad9155",
|
||||
"sha256:d17347e75d6c02864022f52ed608775a5e9ab144d1a82bb40853714f125f5d87"
|
||||
"sha256:24b71f70257e97bab9fdb5ad8342c96eeea1d45bc06a36332978574252219401",
|
||||
"sha256:6709f4581dfd2a00476a134741a738a7f67714ec4f8596c55b22cf3b2ac5a12e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
"version": "==0.6.0"
|
||||
},
|
||||
"django-auditlog": {
|
||||
"hashes": [
|
||||
|
@ -213,14 +213,6 @@
|
|||
"index": "pypi",
|
||||
"version": "==3.7"
|
||||
},
|
||||
"django-formtools": {
|
||||
"hashes": [
|
||||
"sha256:deb932be55b1d9419e37dc4d65dfbfeb8d307b71c8c11fd52f159aba5fc0deed",
|
||||
"sha256:f5f32f62ec8192cd1bc55bd929ca7dff5a5f2addf9027db95a5906ecfaa64836"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4"
|
||||
},
|
||||
"django-fsm": {
|
||||
"hashes": [
|
||||
"sha256:e2c02cbf273fb9691aa9a907c29990afdd21a4adea09c5640344c93fbe03f8d9",
|
||||
|
@ -229,17 +221,6 @@
|
|||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"django-phonenumber-field": {
|
||||
"extras": [
|
||||
"phonenumberslite"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:969bbcab203d697ea44c38726bdc7d72ac9f1ba397694b9f57422b471ad73590",
|
||||
"sha256:9e2b302f239e4703fa9030e44833db5eb524a731335fd77b0b703bd352fbe8d0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0.1"
|
||||
},
|
||||
"django-widget-tweaks": {
|
||||
"hashes": [
|
||||
"sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e",
|
||||
|
@ -278,7 +259,7 @@
|
|||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"gunicorn": {
|
||||
|
@ -382,13 +363,6 @@
|
|||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
},
|
||||
"phonenumberslite": {
|
||||
"hashes": [
|
||||
"sha256:2b7452fe69c907b7638ff4cf7f8a773a6dfce26bf32d67ebf4dc74d5e31abb79",
|
||||
"sha256:b7f56b77711e6b99c7890f20f1aaa705d9fe2514215446b8426c8515c198775f"
|
||||
],
|
||||
"version": "==8.13.3"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50",
|
||||
|
@ -517,7 +491,7 @@
|
|||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.2"
|
||||
},
|
||||
"python-dotenv": {
|
||||
|
@ -549,7 +523,7 @@
|
|||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
|
@ -713,7 +687,7 @@
|
|||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mypy": {
|
||||
|
@ -796,7 +770,7 @@
|
|||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
"sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.10.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
|
@ -804,7 +778,7 @@
|
|||
"sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
|
||||
"sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
|
@ -850,7 +824,7 @@
|
|||
"sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
|
||||
"sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==6.0"
|
||||
},
|
||||
"six": {
|
||||
|
@ -858,7 +832,7 @@
|
|||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"smmap": {
|
||||
|
@ -866,7 +840,7 @@
|
|||
"sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94",
|
||||
"sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"soupsieve": {
|
||||
|
@ -874,7 +848,7 @@
|
|||
"sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759",
|
||||
"sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.3.2.post1"
|
||||
},
|
||||
"sqlparse": {
|
||||
|
@ -898,7 +872,7 @@
|
|||
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"markers": "python_full_version < '3.11.0a7'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"types-cachetools": {
|
||||
|
@ -951,7 +925,7 @@
|
|||
"sha256:7500c9625927c8ec60f54377d590f67b30c8e70ef4b8894214ac6e4cad233d2a",
|
||||
"sha256:780a4082c5fbc0fde6a2fcfe5e26e6efc1e8f425730863c04085769781f51eba"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==2.1.2"
|
||||
},
|
||||
"webob": {
|
||||
|
@ -959,7 +933,7 @@
|
|||
"sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b",
|
||||
"sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.8.7"
|
||||
},
|
||||
"webtest": {
|
||||
|
@ -967,7 +941,7 @@
|
|||
"sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead",
|
||||
"sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.0' and python_version < '4'",
|
||||
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||
"version": "==3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -371,11 +371,13 @@ LOGGING = {
|
|||
"django": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
# Django's template processor
|
||||
"django.template": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
# Django's runserver
|
||||
"django.server": {
|
||||
|
@ -393,16 +395,19 @@ LOGGING = {
|
|||
"oic": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
# Django wrapper for OpenID Connect
|
||||
"djangooidc": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": False,
|
||||
},
|
||||
# Our app!
|
||||
"registrar": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
# root logger catches anything, unless
|
||||
|
|
|
@ -9,30 +9,60 @@ from django.contrib import admin
|
|||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from registrar.views import health, index, profile, whoami
|
||||
from registrar.forms import ApplicationWizard, WIZARD_CONDITIONS
|
||||
from registrar import views
|
||||
from registrar.views.application import Step
|
||||
from registrar.views.utility import always_404
|
||||
from api.views import available
|
||||
|
||||
APPLICATION_URL_NAME = "application_step"
|
||||
application_wizard = ApplicationWizard.as_view(
|
||||
url_name=APPLICATION_URL_NAME,
|
||||
done_step_name="finished",
|
||||
condition_dict=WIZARD_CONDITIONS,
|
||||
)
|
||||
APPLICATION_NAMESPACE = views.ApplicationWizard.URL_NAMESPACE
|
||||
application_urls = [
|
||||
path("", views.ApplicationWizard.as_view(), name=""),
|
||||
path("finished/", views.Finished.as_view(), name="finished"),
|
||||
]
|
||||
|
||||
# dynamically generate the other application_urls
|
||||
for step, view in [
|
||||
# add/remove steps here
|
||||
(Step.ORGANIZATION_TYPE, views.OrganizationType),
|
||||
(Step.ORGANIZATION_FEDERAL, views.OrganizationFederal),
|
||||
(Step.ORGANIZATION_ELECTION, views.OrganizationElection),
|
||||
(Step.ORGANIZATION_CONTACT, views.OrganizationContact),
|
||||
(Step.AUTHORIZING_OFFICIAL, views.AuthorizingOfficial),
|
||||
(Step.CURRENT_SITES, views.CurrentSites),
|
||||
(Step.DOTGOV_DOMAIN, views.DotgovDomain),
|
||||
(Step.PURPOSE, views.Purpose),
|
||||
(Step.YOUR_CONTACT, views.YourContact),
|
||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||
(Step.SECURITY_EMAIL, views.SecurityEmail),
|
||||
(Step.ANYTHING_ELSE, views.AnythingElse),
|
||||
(Step.REQUIREMENTS, views.Requirements),
|
||||
(Step.REVIEW, views.Review),
|
||||
]:
|
||||
application_urls.append(path(f"{step}/", view.as_view(), name=step))
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("", index.index, name="home"),
|
||||
path("whoami/", whoami.whoami, name="whoami"),
|
||||
path("", views.index, name="home"),
|
||||
path("whoami/", views.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(
|
||||
"application/<id>/edit/",
|
||||
views.ApplicationWizard.as_view(),
|
||||
name=views.ApplicationWizard.EDIT_URL_NAME,
|
||||
),
|
||||
path("health/", views.health),
|
||||
path("edit_profile/", views.edit_profile, name="edit-profile"),
|
||||
path("openid/", include("djangooidc.urls")),
|
||||
path("register/", application_wizard, name="application"),
|
||||
path("register/<step>/", application_wizard, name=APPLICATION_URL_NAME),
|
||||
path("register/", include((application_urls, APPLICATION_NAMESPACE))),
|
||||
path("api/v1/available/<domain>", available, name="available"),
|
||||
path(
|
||||
"todo",
|
||||
lambda r: always_404(r, "We forgot to include this link, sorry."),
|
||||
name="todo",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += [
|
||||
# redirect to login.gov
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
from .edit_profile import EditProfileForm
|
||||
from .application_wizard import ApplicationWizard, WIZARD_CONDITIONS
|
||||
|
||||
__all__ = ["EditProfileForm", "ApplicationWizard", "WIZARD_CONDITIONS"]
|
||||
from .edit_profile import *
|
||||
from .application_wizard import *
|
||||
|
|
|
@ -1,26 +1,14 @@
|
|||
"""Forms Wizard for creating a new domain application."""
|
||||
|
||||
from __future__ import annotations # allows forward references in annotations
|
||||
|
||||
import logging
|
||||
|
||||
from typing import Union
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import resolve
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from formtools.wizard.views import NamedUrlSessionWizardView # type: ignore
|
||||
from formtools.wizard.storage.session import SessionStorage # type: ignore
|
||||
|
||||
from phonenumber_field.formfields import PhoneNumberField # type: ignore
|
||||
|
||||
from registrar.models import Contact, DomainApplication, Domain
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# nosec because this use of mark_safe does not introduce a cross-site scripting
|
||||
|
@ -32,10 +20,19 @@ REQUIRED_SUFFIX = mark_safe( # nosec
|
|||
|
||||
|
||||
class RegistrarForm(forms.Form):
|
||||
"""Subclass used to remove the default colon suffix from all fields."""
|
||||
"""
|
||||
A common set of methods and configuration.
|
||||
|
||||
The registrar's domain application is several pages of "steps".
|
||||
Each step is an HTML form containing one or more Django "forms".
|
||||
|
||||
Subclass this class to create new forms.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("label_suffix", "")
|
||||
# save a reference to an application object
|
||||
self.application = kwargs.pop("application", None)
|
||||
super(RegistrarForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_database(self, obj: DomainApplication | Contact):
|
||||
|
@ -50,10 +47,14 @@ class RegistrarForm(forms.Form):
|
|||
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
|
||||
@classmethod
|
||||
def from_database(cls, obj: DomainApplication | Contact | None):
|
||||
"""Returns a dict of form field values gotten from `obj`."""
|
||||
if obj is None:
|
||||
return {}
|
||||
return {
|
||||
name: getattr(obj, name) for name in cls.declared_fields.keys()
|
||||
} # type: ignore
|
||||
|
||||
|
||||
class OrganizationTypeForm(RegistrarForm):
|
||||
|
@ -142,10 +143,11 @@ class OrganizationContactForm(RegistrarForm):
|
|||
def clean_federal_agency(self):
|
||||
"""Require something to be selected when this is a federal agency."""
|
||||
federal_agency = self.cleaned_data.get("federal_agency", None)
|
||||
# need the wizard object to know if this is federal
|
||||
context = self.get_context()
|
||||
print(context)
|
||||
if wizard._is_federal():
|
||||
# need the application object to know if this is federal
|
||||
if self.application is None:
|
||||
# hmm, no saved application object?
|
||||
raise ValueError("Form has no active application object.")
|
||||
if self.application.is_federal:
|
||||
if not federal_agency:
|
||||
# no answer was selected
|
||||
raise forms.ValidationError("Please select your federal agency.", code="required")
|
||||
|
@ -154,7 +156,6 @@ class OrganizationContactForm(RegistrarForm):
|
|||
|
||||
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)
|
||||
|
@ -166,11 +167,10 @@ class AuthorizingOfficialForm(RegistrarForm):
|
|||
obj.authorizing_official = contact
|
||||
obj.save()
|
||||
|
||||
def from_database(self, obj):
|
||||
"""Initializes this form's fields with values gotten from `obj`."""
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
contact = getattr(obj, "authorizing_official", None)
|
||||
if contact is not None:
|
||||
super().from_database(contact)
|
||||
return super().from_database(contact)
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="First name/given name",
|
||||
|
@ -201,7 +201,6 @@ 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()
|
||||
|
@ -210,11 +209,13 @@ class CurrentSitesForm(RegistrarForm):
|
|||
# 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`."""
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
current_website = obj.current_websites.first()
|
||||
if current_website is not None:
|
||||
self.initial["current_site"] = current_website.website
|
||||
return {"current_site": current_website.website}
|
||||
else:
|
||||
return {}
|
||||
|
||||
current_site = forms.CharField(
|
||||
required=False,
|
||||
|
@ -246,7 +247,6 @@ class CurrentSitesForm(RegistrarForm):
|
|||
|
||||
class DotGovDomainForm(RegistrarForm):
|
||||
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(
|
||||
|
@ -270,15 +270,18 @@ class DotGovDomainForm(RegistrarForm):
|
|||
# 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`."""
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
values = {}
|
||||
requested_domain = getattr(obj, "requested_domain", None)
|
||||
if requested_domain is not None:
|
||||
self.initial["requested_domain"] = requested_domain.sld
|
||||
values["requested_domain"] = requested_domain.sld
|
||||
|
||||
alternative_domain = obj.alternative_domains.first()
|
||||
if alternative_domain is not None:
|
||||
self.initial["alternative_domain"] = alternative_domain.sld
|
||||
values["alternative_domain"] = alternative_domain.sld
|
||||
|
||||
return values
|
||||
|
||||
requested_domain = forms.CharField(
|
||||
label="What .gov domain do you want?",
|
||||
|
@ -329,7 +332,6 @@ class PurposeForm(RegistrarForm):
|
|||
|
||||
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)
|
||||
|
@ -341,11 +343,10 @@ class YourContactForm(RegistrarForm):
|
|||
obj.submitter = contact
|
||||
obj.save()
|
||||
|
||||
def from_database(self, obj):
|
||||
"""Initializes this form's fields with values gotten from `obj`."""
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
contact = getattr(obj, "submitter", None)
|
||||
if contact is not None:
|
||||
super().from_database(contact)
|
||||
return super().from_database(contact)
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="First name/given name",
|
||||
|
@ -376,7 +377,6 @@ 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()
|
||||
|
@ -390,11 +390,10 @@ class OtherContactsForm(RegistrarForm):
|
|||
super().to_database(contact)
|
||||
obj.other_contacts.add(contact)
|
||||
|
||||
def from_database(self, obj):
|
||||
"""Initializes this form's fields with values gotten from `obj`."""
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
other_contacts = obj.other_contacts.first()
|
||||
if other_contacts is not None:
|
||||
super().from_database(other_contacts)
|
||||
return super().from_database(other_contacts)
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="First name/given name",
|
||||
|
@ -447,266 +446,3 @@ class RequirementsForm(RegistrarForm):
|
|||
),
|
||||
required=False, # use field validation to enforce this
|
||||
)
|
||||
|
||||
def clean_is_policy_acknowledged(self):
|
||||
"""This box must be checked to proceed but offer a clear error."""
|
||||
# already converted to a boolean
|
||||
is_acknowledged = self.cleaned_data["is_policy_acknowledged"]
|
||||
if not is_acknowledged:
|
||||
raise forms.ValidationError(
|
||||
"You must read and agree to the .gov domain requirements to proceed.",
|
||||
code="invalid",
|
||||
)
|
||||
return is_acknowledged
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 = [
|
||||
(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.
|
||||
TEMPLATES = {
|
||||
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
|
||||
TITLES = {
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
# We can use a dictionary with step names and callables that return booleans
|
||||
# to show or hide particular steps based on the state of the process.
|
||||
WIZARD_CONDITIONS = {
|
||||
"organization_federal": DomainApplication.show_organization_federal,
|
||||
"organization_election": DomainApplication.show_organization_election,
|
||||
}
|
||||
|
||||
|
||||
class TrackingStorage(SessionStorage):
|
||||
|
||||
"""Storage subclass that keeps track of what the current_step has been."""
|
||||
|
||||
def _set_current_step(self, step):
|
||||
super()._set_current_step(step)
|
||||
|
||||
step_history = self.extra_data.setdefault("step_history", [])
|
||||
# can't serialize a set, so keep list entries unique
|
||||
if step not in step_history:
|
||||
step_history.append(step)
|
||||
|
||||
|
||||
class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
|
||||
|
||||
"""Multi-page form ("wizard") for new domain applications.
|
||||
|
||||
This sets up a sequence of forms that gather information for new
|
||||
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
|
||||
storage_name = "registrar.forms.application_wizard.TrackingStorage"
|
||||
|
||||
def get_template_names(self):
|
||||
"""Template for the current step.
|
||||
|
||||
The return is a singleton list.
|
||||
"""
|
||||
return [TEMPLATES[self.steps.current]]
|
||||
|
||||
def _is_federal(self) -> Union[bool, None]:
|
||||
"""Return whether this application is from a federal agency.
|
||||
|
||||
Returns True if we know that this application is from a federal
|
||||
agency, False if we know that it is not and None if there isn't an
|
||||
answer yet for that question.
|
||||
"""
|
||||
return self.get_application_object().is_federal()
|
||||
|
||||
def get_context_data(self, form, **kwargs):
|
||||
"""Add title information to the context for all steps."""
|
||||
context = super().get_context_data(form=form, **kwargs)
|
||||
context["form_titles"] = TITLES
|
||||
|
||||
# Add information about which steps should be unlocked
|
||||
# TODO: sometimes the first step doesn't get added to the step history
|
||||
# so add it here
|
||||
context["visited"] = self.storage.extra_data.get("step_history", []) + [
|
||||
self.steps.first
|
||||
]
|
||||
|
||||
if self.steps.current == Step.ORGANIZATION_CONTACT:
|
||||
context["is_federal"] = self._is_federal()
|
||||
if self.steps.current == Step.REVIEW:
|
||||
context["step_cls"] = Step
|
||||
application = self.get_application_object()
|
||||
context["application"] = application
|
||||
return context
|
||||
|
||||
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)
|
||||
self.storage.extra_data["application_id"] = application.id
|
||||
return application
|
||||
|
||||
def form_to_database(self, form: RegistrarForm) -> DomainApplication:
|
||||
"""
|
||||
Unpack the form responses onto the model object properties.
|
||||
|
||||
Saves the application to the database.
|
||||
"""
|
||||
application = self.get_application_object()
|
||||
|
||||
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.form_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):
|
||||
"""Called when the data for every form is submitted and validated."""
|
||||
application = self.get_application_object()
|
||||
application.submit() # change the status to submitted
|
||||
application.save()
|
||||
logger.debug("Application object saved: %s", application.id)
|
||||
return render(
|
||||
self.request, "application_done.html", {"application_id": application.id}
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import Union
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
|
@ -7,9 +7,6 @@ from django_fsm import FSMField, transition # type: ignore
|
|||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..forms.application_wizard import ApplicationWizard
|
||||
|
||||
|
||||
class DomainApplication(TimeStampedModel):
|
||||
|
||||
|
@ -460,30 +457,17 @@ class DomainApplication(TimeStampedModel):
|
|||
# during the application flow. They are policies about the application so
|
||||
# they appear here.
|
||||
|
||||
@staticmethod
|
||||
def _get_organization_type(wizard: ApplicationWizard) -> Union[str, None]:
|
||||
"""Extract the answer to the organization type question from the wizard."""
|
||||
# using the step data from the storage is a workaround for this
|
||||
# bug in django-formtools version 2.4
|
||||
# https://github.com/jazzband/django-formtools/issues/220
|
||||
type_data = wizard.storage.get_step_data("organization_type")
|
||||
if type_data:
|
||||
return type_data.get("organization_type-organization_type")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def show_organization_federal(wizard: ApplicationWizard) -> bool:
|
||||
def show_organization_federal(self) -> bool:
|
||||
"""Show this step if the answer to the first question was "federal"."""
|
||||
user_choice = DomainApplication._get_organization_type(wizard)
|
||||
user_choice = self.organization_type
|
||||
return user_choice == DomainApplication.OrganizationChoices.FEDERAL
|
||||
|
||||
@staticmethod
|
||||
def show_organization_election(wizard: ApplicationWizard) -> bool:
|
||||
def show_organization_election(self) -> bool:
|
||||
"""Show this step if the answer to the first question implies it.
|
||||
|
||||
This shows for answers that aren't "Federal" or "Interstate".
|
||||
"""
|
||||
user_choice = DomainApplication._get_organization_type(wizard)
|
||||
user_choice = self.organization_type
|
||||
excluded = [
|
||||
DomainApplication.OrganizationChoices.FEDERAL,
|
||||
DomainApplication.OrganizationChoices.INTERSTATE,
|
||||
|
|
|
@ -6,14 +6,13 @@
|
|||
|
||||
<p id="instructions">Is there anything else we should know about your domain request?</p>
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post">
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post">
|
||||
<div class="usa-form-group">
|
||||
{{ wizard.management_form }}
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="usa-character-count">
|
||||
{{ wizard.form.anything_else|add_label_class:"usa-label usa-sr-only" }}
|
||||
{{ wizard.form.anything_else|add_class:"usa-textarea usa-character-count__field"|attr:"aria-describedby:instructions"|attr:"maxlength=500" }}
|
||||
{{ forms.0.anything_else|add_label_class:"usa-label usa-sr-only" }}
|
||||
{{ forms.0.anything_else|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>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<h2>Who is the authorizing official for your organization?</h2>
|
||||
|
||||
<div id="instructions">
|
||||
<p>Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest ranking or highest elected official in your organization. Read more about <a href="#">who can serve as an authorizing official</a>.
|
||||
<p>Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest ranking or highest elected official in your organization. Read more about <a href="{% url 'todo' %}">who can serve as an authorizing official</a>.
|
||||
</p>
|
||||
|
||||
<div class="ao-example">
|
||||
|
@ -21,8 +21,7 @@
|
|||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" id="step__{{wizard.steps.current}}" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
|
@ -30,17 +29,17 @@
|
|||
Who is the authorizing official for your organization?
|
||||
</legend>
|
||||
|
||||
{% input_with_errors wizard.form.first_name %}
|
||||
{% input_with_errors forms.0.first_name %}
|
||||
|
||||
{% input_with_errors wizard.form.middle_name %}
|
||||
{% input_with_errors forms.0.middle_name %}
|
||||
|
||||
{% input_with_errors wizard.form.last_name %}
|
||||
{% input_with_errors forms.0.last_name %}
|
||||
|
||||
{% input_with_errors wizard.form.title %}
|
||||
{% input_with_errors forms.0.title %}
|
||||
|
||||
{% input_with_errors wizard.form.email %}
|
||||
{% input_with_errors forms.0.email %}
|
||||
|
||||
{% input_with_errors wizard.form.phone add_class="usa-input--medium" %}
|
||||
{% input_with_errors forms.0.phone add_class="usa-input--medium" %}
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
|
||||
{% block form_content %}
|
||||
|
||||
<form class="usa-form usa-form--large" id="step__{{wizard.steps.current}}" method="post">
|
||||
{{ wizard.management_form }}
|
||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% input_with_errors wizard.form.current_site %}
|
||||
{% input_with_errors forms.0.form.current_site %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ your authorizing official, and any contacts you added.</p>
|
|||
meets our naming requirements.
|
||||
</li>
|
||||
|
||||
<li> You can <a href="/application/{{ application_id }}"><del>check the
|
||||
<li> You can <a href="{% url 'todo' %}"><del>check the
|
||||
status</del></a> of your request at any time.
|
||||
</li>
|
||||
|
||||
|
@ -32,9 +32,9 @@ your authorizing official, and any contacts you added.</p>
|
|||
<p>Before your domain can be used we'll need information about your
|
||||
domain name servers. If you have this information you can enter it now.
|
||||
If you don't have it, that's okay. You can enter it later on the
|
||||
<a href="/domains"><del>manage your domains page</del></a>.
|
||||
<a href="{% url 'todo' %}"><del>manage your domains page</del></a>.
|
||||
</p>
|
||||
|
||||
<p><a href="/application/{{ application_id }}#nameservers" class="usa-button">Enter DNS name servers</a></p>
|
||||
<p><a href="{% url 'todo' %}" class="usa-button">Enter DNS name servers</a></p>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% load widget_tweaks static%}
|
||||
|
||||
{% block form_content %}
|
||||
<p> Before requesting a .gov domain, <a href="#">please make sure it meets our naming requirements.</a> Your domain name must:
|
||||
<p> Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it meets our naming requirements.</a> Your domain name must:
|
||||
<ul class="usa-list">
|
||||
<li>Be available </li>
|
||||
<li>Be unique </li>
|
||||
|
@ -21,12 +21,11 @@
|
|||
{% include "includes/domain_example__city.html" %}
|
||||
</div>
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<h2> What .gov domain do you want? </h2>
|
||||
<p class="domain_instructions"> After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all of our requirements once you complete and submit the rest of this form. </p>
|
||||
|
||||
<p> This question is required. </p>
|
||||
{{ wizard.management_form }}
|
||||
{% csrf_token %}
|
||||
|
||||
{% if wizard.form.requested_domain.errors %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static widget_tweaks %}
|
||||
{% load static widget_tweaks namespaced_urls %}
|
||||
|
||||
{% block title %}Apply for a .gov domain – {{form_titles|get_item:wizard.steps.current}}{% endblock %}
|
||||
{% block title %}Apply for a .gov domain – {{form_titles|get_item:steps.current}}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row grid-gap">
|
||||
|
@ -10,8 +10,8 @@
|
|||
</div>
|
||||
<div class="grid-col-9">
|
||||
<main id="main-content" class="grid-container register-form-step">
|
||||
{% if wizard.steps.prev %}
|
||||
<a href="{% url wizard.url_name step=wizard.steps.prev %}" class="breadcrumb__back">
|
||||
{% if steps.prev %}
|
||||
<a href="{% namespaced_url 'application' steps.prev %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||
</svg><span class="margin-left-05">Previous step </span>
|
||||
|
@ -37,12 +37,11 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<h1> {{form_titles|get_item:wizard.steps.current}} </h1>
|
||||
|
||||
<h1> {{form_titles|get_item:steps.current}} </h1>
|
||||
{% block form_content %}
|
||||
|
||||
<div class="stepnav">
|
||||
{% if wizard.steps.next %}
|
||||
{% if steps.next %}
|
||||
<button
|
||||
type="submit"
|
||||
name="submit_button"
|
||||
|
|
|
@ -16,30 +16,29 @@
|
|||
{% include "includes/required_fields.html" %}
|
||||
</div>
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend class="usa-sr-only">What is the name and mailing address of your organization?</legend>
|
||||
|
||||
{% if is_federal %}
|
||||
{% select_with_errors wizard.form.federal_agency required=True %}
|
||||
{% select_with_errors forms.0.federal_agency required=True %}
|
||||
{% endif %}
|
||||
|
||||
{% input_with_errors wizard.form.organization_name %}
|
||||
{% input_with_errors forms.0.organization_name %}
|
||||
|
||||
{% input_with_errors wizard.form.address_line1 %}
|
||||
{% input_with_errors forms.0.address_line1 %}
|
||||
|
||||
{% input_with_errors wizard.form.address_line2 %}
|
||||
{% input_with_errors forms.0.address_line2 %}
|
||||
|
||||
{% input_with_errors wizard.form.city %}
|
||||
{% input_with_errors forms.0.city %}
|
||||
|
||||
{% select_with_errors wizard.form.state_territory %}
|
||||
{% select_with_errors forms.0.state_territory %}
|
||||
|
||||
{% input_with_errors wizard.form.zipcode add_class="usa-input--small" %}
|
||||
{% input_with_errors forms.0.zipcode add_class="usa-input--small" %}
|
||||
|
||||
{% input_with_errors wizard.form.urbanization %}
|
||||
{% input_with_errors forms.0.urbanization %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
|
|
|
@ -5,15 +5,14 @@
|
|||
|
||||
{% block form_content %}
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<fieldset id="election_board__fieldset" class="usa-fieldset">
|
||||
<legend>
|
||||
<h2 class="margin-bottom-05">Is your organization an election office?</h2>
|
||||
<p> This question is required.</p>
|
||||
</legend>
|
||||
{% radio_buttons_by_value wizard.form.is_election_board as choices %}
|
||||
{% radio_buttons_by_value forms.0.is_election_board as choices %}
|
||||
{% for choice in choices.values %}
|
||||
{% include "includes/radio_button.html" with choice=choice tile="true" required="true"%}
|
||||
{% endfor %}
|
||||
|
|
|
@ -5,15 +5,14 @@
|
|||
|
||||
{% block form_content %}
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<fieldset id="federal_type__fieldset" class="usa-fieldset">
|
||||
<legend>
|
||||
<h2>Which federal branch is your organization in?</h2>
|
||||
<p class="margin-bottom-5">This question is required.</p>
|
||||
</legend>
|
||||
{% radio_buttons_by_value wizard.form.federal_type as choices %}
|
||||
{% radio_buttons_by_value forms.0.federal_type as choices %}
|
||||
{% for choice in choices.values %}
|
||||
{% include "includes/radio_button.html" with choice=choice tile="true" %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
|
||||
{% block form_content %}
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% radio_buttons_by_value wizard.form.organization_type as choices %}
|
||||
{% radio_buttons_by_value forms.0.organization_type as choices %}
|
||||
|
||||
<fieldset id="organization_type__fieldset" class="usa-fieldset">
|
||||
<legend class="usa-legend">
|
||||
<h2> What kind of U.S.-based government organization do you represent? </h2>
|
||||
<p>This question is required.</p>
|
||||
</legend>
|
||||
{{ forms.0.organization_type.errors }}
|
||||
{% for choice in choices.values %}
|
||||
{% include "includes/radio_button.html" with choice=choice tile="true" %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -9,25 +9,24 @@
|
|||
<p id="instructions">We’d like to contact other employees with administrative or technical responsibilities in your organization. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility and understand the purpose of the .gov domain. These contacts should be in addition to you and your authorizing official. </p>
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" id="step__{{wizard.steps.current}}" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend>
|
||||
<h2 class="margin-bottom-05"> Contact 2 </h2>
|
||||
</legend>
|
||||
{% input_with_errors wizard.form.first_name %}
|
||||
{% input_with_errors forms.0.first_name %}
|
||||
|
||||
{% input_with_errors wizard.form.middle_name %}
|
||||
{% input_with_errors forms.0.middle_name %}
|
||||
|
||||
{% input_with_errors wizard.form.last_name %}
|
||||
{% input_with_errors forms.0.last_name %}
|
||||
|
||||
{% input_with_errors wizard.form.title %}
|
||||
{% input_with_errors forms.0.title %}
|
||||
|
||||
{% input_with_errors wizard.form.email %}
|
||||
{% input_with_errors forms.0.email %}
|
||||
|
||||
{% input_with_errors wizard.form.phone add_class="usa-input--medium" %}
|
||||
{% input_with_errors forms.0.phone add_class="usa-input--medium" %}
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -10,13 +10,12 @@
|
|||
|
||||
<p> This question is required. </p>
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<div class="usa-form-group">
|
||||
{{ wizard.management_form }}
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="usa-character-count">
|
||||
{% with field=wizard.form.purpose %}
|
||||
{% with field=forms.0.purpose %}
|
||||
{% if field.errors %}
|
||||
<div class="usa-form-group usa-form-group--error">
|
||||
{{ field|add_label_class:"usa-label usa-label--error usa-sr-only" }}
|
||||
|
|
|
@ -129,27 +129,26 @@
|
|||
|
||||
<p>This question is required.</p>
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post" novalidate>
|
||||
<div class="usa-form-group">
|
||||
{{ wizard.management_form }}
|
||||
{% csrf_token %}
|
||||
|
||||
{% if wizard.form.is_policy_acknowledged.errors %}
|
||||
{% if forms.0.is_policy_acknowledged.errors %}
|
||||
<div class="usa-form-group usa-form-group--error">
|
||||
{% for error in wizard.form.is_policy_acknowledged.errors %}
|
||||
{% for error in forms.0.is_policy_acknowledged.errors %}
|
||||
<span class="usa-error-message margin-bottom-1" id="input-error-message" role="alert">
|
||||
{{ error }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
<div class="usa-checkbox">
|
||||
{{ wizard.form.is_policy_acknowledged|add_class:"usa-checkbox__input"|add_class:"usa-input--error"|attr:"aria-invalid:true"|attr:"required" }}
|
||||
{{ wizard.form.is_policy_acknowledged|add_label_class:"usa-checkbox__label usa-label--error" }}
|
||||
{{ forms.0.is_policy_acknowledged|add_class:"usa-checkbox__input"|add_class:"usa-input--error"|attr:"aria-invalid:true"|attr:"required" }}
|
||||
{{ forms.0.is_policy_acknowledged|add_label_class:"usa-checkbox__label usa-label--error" }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="usa-checkbox">
|
||||
{{ wizard.form.is_policy_acknowledged|add_class:"usa-checkbox__input"|attr:"required"}}
|
||||
{{ wizard.form.is_policy_acknowledged|add_label_class:"usa-checkbox__label" }}
|
||||
{{ forms.0.is_policy_acknowledged|add_class:"usa-checkbox__input"|attr:"required"}}
|
||||
{{ forms.0.is_policy_acknowledged|add_label_class:"usa-checkbox__label" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,44 +1,43 @@
|
|||
<!-- Test page -->
|
||||
{% extends 'application_form.html' %}
|
||||
{% load static widget_tweaks %}
|
||||
{% load static widget_tweaks namespaced_urls %}
|
||||
|
||||
{% block form_content %}
|
||||
|
||||
<form id="step__{{wizard.steps.current}}" class="usa-form usa-form--large" method="post">
|
||||
{{ wizard.management_form }}
|
||||
<form id="step__{{steps.current}}" class="usa-form usa-form--large" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% for step in wizard.steps.all|slice:":-1" %}
|
||||
{% for step in steps.all|slice:":-1" %}
|
||||
<section class="review__step margin-top-205">
|
||||
<hr />
|
||||
<div class="review__step__title display-flex flex-justify">
|
||||
<div class="review__step__value">
|
||||
<div class="review__step__name">{{ form_titles|get_item:step }}</div>
|
||||
<div>
|
||||
{% if step == step_cls.ORGANIZATION_TYPE %}
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{{ application.get_organization_type_display|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.ORGANIZATION_FEDERAL %}
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{{ application.get_federal_type_display|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.ORGANIZATION_ELECTION %}
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.ORGANIZATION_CONTACT %}
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% if application.organization_name %}
|
||||
{% include "includes/organization_address.html" with organization=application %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == step_cls.AUTHORIZING_OFFICIAL %}
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% if application.authorizing_official %}
|
||||
{% include "includes/contact.html" with contact=application.authorizing_official %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == step_cls.CURRENT_SITES %}
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
<ul class="add-list-reset">
|
||||
{% for site in application.current_websites.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
|
@ -47,7 +46,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == step_cls.DOTGOV_DOMAIN %}
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
<ul class="add-list-reset">
|
||||
<li>{{ application.requested_domain.name|default:"Incomplete" }}</li>
|
||||
{% for site in application.alternative_domains.all %}
|
||||
|
@ -55,37 +54,37 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == step_cls.PURPOSE %}
|
||||
{% if step == Step.PURPOSE %}
|
||||
{{ application.purpose|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.YOUR_CONTACT %}
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% if application.submitter %}
|
||||
{% include "includes/contact.html" with contact=application.submitter %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == step_cls.OTHER_CONTACTS %}
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% for other in application.other_contacts.all %}
|
||||
{% include "includes/contact.html" with contact=other %}
|
||||
{% empty %}
|
||||
None
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if step == step_cls.SECURITY_EMAIL %}
|
||||
{% if step == Step.SECURITY_EMAIL %}
|
||||
{{ application.security_email|default:"None" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.ANYTHING_ELSE %}
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{{ application.anything_else|default:"No" }}
|
||||
{% endif %}
|
||||
{% if step == step_cls.REQUIREMENTS %}
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{{ application.is_policy_acknowledged|yesno:"Agree,Do not agree,Do not agree" }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
aria-describedby="review_step_title__{{step}}"
|
||||
href="{% url wizard.url_name step=step %}"
|
||||
href="{% namespaced_url 'application' step %}"
|
||||
>Edit<span class="sr-only"> {{ form_titles|get_item:step }}</span></a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
<p id="instructions"> We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. <strong> Security emails are made public.</strong> We recommend using an alias, like security@<domain.gov>.</p>
|
||||
|
||||
<form class="usa-form usa-form--large" id="step__{{wizard.steps.current}}" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% if wizard.form.security_email.errors %}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
{% load static %}
|
||||
{% load static namespaced_urls %}
|
||||
|
||||
<div class="margin-bottom-4 tablet:margin-bottom-0">
|
||||
<nav aria-label="Form steps,">
|
||||
<ul class="usa-sidenav">
|
||||
{% for this_step in wizard.steps.all %}
|
||||
{% for this_step in steps.all %}
|
||||
{% if this_step in visited %}
|
||||
<li class="usa-sidenav__item">
|
||||
<a href="{% url wizard.url_name step=this_step %}"
|
||||
{% if this_step == wizard.steps.current %}class="usa-current"{% endif%}>
|
||||
<a href="{% namespaced_url 'application' this_step %}"
|
||||
{% if this_step == steps.current %}class="usa-current"{% endif%}>
|
||||
{{ form_titles|get_item:this_step }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
|
|
@ -16,25 +16,24 @@
|
|||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" id="step__{{wizard.steps.current}}" method="post" novalidate>
|
||||
{{ wizard.management_form }}
|
||||
<form class="usa-form usa-form--large" id="step__{{steps.current}}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend class="usa-sr-only">
|
||||
Your contact information
|
||||
</legend>
|
||||
{% input_with_errors wizard.form.first_name %}
|
||||
{% input_with_errors forms.0.first_name %}
|
||||
|
||||
{% input_with_errors wizard.form.middle_name %}
|
||||
{% input_with_errors forms.0.middle_name %}
|
||||
|
||||
{% input_with_errors wizard.form.last_name %}
|
||||
{% input_with_errors forms.0.last_name %}
|
||||
|
||||
{% input_with_errors wizard.form.title %}
|
||||
{% input_with_errors forms.0.title %}
|
||||
|
||||
{% input_with_errors wizard.form.email %}
|
||||
{% input_with_errors forms.0.email %}
|
||||
|
||||
{% input_with_errors wizard.form.phone add_class="usa-input--medium" %}
|
||||
{% input_with_errors forms.0.phone add_class="usa-input--medium" %}
|
||||
</fieldset>
|
||||
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@
|
|||
{% block logo %}
|
||||
<div class="usa-logo" id="extended-logo">
|
||||
<em class="usa-logo__text">
|
||||
<a href="/" title="Home" aria-label="Home">
|
||||
<a href="{% url 'home' %}" title="Home" aria-label="Home">
|
||||
{% block site_name %}Home{% endblock %}
|
||||
</a>
|
||||
</em>
|
||||
|
@ -145,14 +145,14 @@
|
|||
<ul class="usa-nav__primary usa-accordion">
|
||||
<li class="usa-nav__primary-item">
|
||||
{% if user.is_authenticated %}
|
||||
<a href="/whoami"><span>{{ user.email }}</span></a>
|
||||
<a href="{% url 'whoami' %}"><span>{{ user.email }}</span></a>
|
||||
</li>
|
||||
<li class="usa-nav__primary-item display-flex flex-align-center">
|
||||
<span class="text-base"> | </span>
|
||||
<a href="/openid/logout"><span class="text-primary">Sign out</span></a>
|
||||
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<a href="/openid/login"><span>Sign in</span></a>
|
||||
<a href="{% url 'login' %}"><span>Sign in</span></a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
</table>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="{% url 'application' %}" class="usa-button">Apply</a></p>
|
||||
<p><a href="{% url 'application:' %}" class="usa-button">Apply</a></p>
|
||||
|
||||
<p><a href="{% url 'edit-profile' %}">Edit profile</a></p>
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
>
|
||||
</li>
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a href="TODO" class="usa-identifier__required-link usa-link"
|
||||
<a href="{% url 'todo' %}" class="usa-identifier__required-link usa-link"
|
||||
>Vulnerability disclosure policy</a
|
||||
>
|
||||
</li>
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
<main id="main-content" class="grid-container">
|
||||
<p> Hello {{ user.last_name|default:"No last name given" }}, {{ user.first_name|default:"No first name given" }} <{{ user.email }}>! </p>
|
||||
|
||||
<p><a href="/openid/logout">Click here to log out</a></p>
|
||||
<p><a href="{% url 'logout' %}">Click here to log out</a></p>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
|
10
src/registrar/templatetags/namespaced_urls.py
Normal file
10
src/registrar/templatetags/namespaced_urls.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django import template
|
||||
from django.urls import reverse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def namespaced_url(namespace, name="", **kwargs):
|
||||
"""Get a URL, given its Django namespace and name."""
|
||||
return reverse(f"{namespace}:{name}", kwargs=kwargs)
|
|
@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model
|
|||
from django_webtest import WebTest # type: ignore
|
||||
|
||||
from registrar.models import DomainApplication, Domain, Contact, Website
|
||||
from registrar.forms.application_wizard import TITLES, Step
|
||||
from registrar.views.application import ApplicationWizard
|
||||
|
||||
from .common import less_console_noise
|
||||
|
||||
|
@ -101,6 +101,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app.set_user(self.user.username)
|
||||
self.TITLES = ApplicationWizard.TITLES
|
||||
|
||||
def tearDown(self):
|
||||
# delete any applications we made so that users can be deleted
|
||||
|
@ -109,7 +110,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
def test_application_form_empty_submit(self):
|
||||
# 302 redirect to the first form
|
||||
page = self.app.get(reverse("application")).follow()
|
||||
page = self.app.get(reverse("application:")).follow()
|
||||
# submitting should get back the same page if the required field is empty
|
||||
result = page.form.submit()
|
||||
self.assertIn(
|
||||
|
@ -123,9 +124,9 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
"""
|
||||
num_pages_tested = 0
|
||||
SKIPPED_PAGES = 1 # elections
|
||||
num_pages = len(TITLES) - SKIPPED_PAGES
|
||||
num_pages = len(self.TITLES) - SKIPPED_PAGES
|
||||
|
||||
type_page = self.app.get(reverse("application")).follow()
|
||||
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
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -511,6 +512,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
# final submission results in a redirect to the "finished" URL
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
with less_console_noise():
|
||||
review_result = review_form.submit()
|
||||
|
||||
self.assertEquals(review_result.status_code, 302)
|
||||
|
@ -529,7 +531,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
def test_application_form_conditional_federal(self):
|
||||
"""Federal branch question is shown for federal organizations."""
|
||||
type_page = self.app.get(reverse("application")).follow()
|
||||
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
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -539,8 +541,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# ---- TYPE PAGE ----
|
||||
|
||||
# the conditional step titles shouldn't appear initially
|
||||
self.assertNotContains(type_page, TITLES["organization_federal"])
|
||||
self.assertNotContains(type_page, TITLES["organization_election"])
|
||||
self.assertNotContains(type_page, self.TITLES["organization_federal"])
|
||||
self.assertNotContains(type_page, self.TITLES["organization_election"])
|
||||
type_form = type_page.form
|
||||
type_form["organization_type-organization_type"] = "federal"
|
||||
|
||||
|
@ -557,8 +559,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# but the step label for the elections page should not appear
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
federal_page = type_result.follow()
|
||||
self.assertContains(federal_page, TITLES["organization_federal"])
|
||||
self.assertNotContains(federal_page, TITLES["organization_election"])
|
||||
self.assertContains(federal_page, self.TITLES["organization_federal"])
|
||||
self.assertNotContains(federal_page, self.TITLES["organization_election"])
|
||||
|
||||
# continuing on in the flow we need to see top-level agency on the
|
||||
# contact page
|
||||
|
@ -575,7 +577,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
def test_application_form_conditional_elections(self):
|
||||
"""Election question is shown for other organizations."""
|
||||
type_page = self.app.get(reverse("application")).follow()
|
||||
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
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -585,8 +587,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# ---- TYPE PAGE ----
|
||||
|
||||
# the conditional step titles shouldn't appear initially
|
||||
self.assertNotContains(type_page, TITLES["organization_federal"])
|
||||
self.assertNotContains(type_page, TITLES["organization_election"])
|
||||
self.assertNotContains(type_page, self.TITLES["organization_federal"])
|
||||
self.assertNotContains(type_page, self.TITLES["organization_election"])
|
||||
type_form = type_page.form
|
||||
type_form["organization_type-organization_type"] = "county"
|
||||
|
||||
|
@ -602,8 +604,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# but the step label for the elections page should not appear
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
election_page = type_result.follow()
|
||||
self.assertContains(election_page, TITLES["organization_election"])
|
||||
self.assertNotContains(election_page, TITLES["organization_federal"])
|
||||
self.assertContains(election_page, self.TITLES["organization_election"])
|
||||
self.assertNotContains(election_page, self.TITLES["organization_federal"])
|
||||
|
||||
# continuing on in the flow we need to NOT see top-level agency on the
|
||||
# contact page
|
||||
|
@ -622,7 +624,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
def test_application_form_section_skipping(self):
|
||||
"""Can skip forward and back in sections"""
|
||||
type_page = self.app.get(reverse("application")).follow()
|
||||
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
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
|
@ -640,7 +642,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
# Now on federal type page, click back to the organization type
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
new_page = federal_page.click(TITLES[Step.ORGANIZATION_TYPE], index=0)
|
||||
new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0)
|
||||
|
||||
# Should be a link to the organization_federal page
|
||||
self.assertGreater(
|
||||
|
@ -708,7 +710,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# -- the best that can/should be done here is to ensure the correct values
|
||||
# are being passed to the templating engine
|
||||
|
||||
url = reverse("application_step", kwargs={"step": "organization_type"})
|
||||
url = reverse("application:organization_type")
|
||||
response = self.client.get(url, follow=True)
|
||||
self.assertContains(response, "<input>")
|
||||
# choices = response.context['wizard']['form']['organization_type'].subwidgets
|
||||
|
@ -716,62 +718,62 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# checked = radio.data["selected"]
|
||||
# self.assertTrue(checked)
|
||||
|
||||
# url = reverse("application_step", kwargs={"step": "organization_federal"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application: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"})
|
||||
# url = reverse("application:requirements")
|
||||
# self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# page = self.app.get(url)
|
||||
# self.assertNotContains(page, "VALUE")
|
||||
|
|
1
src/registrar/utility/__init__.py
Normal file
1
src/registrar/utility/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .str_enum import *
|
43
src/registrar/utility/str_enum.py
Normal file
43
src/registrar/utility/str_enum.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from enum import Enum, EnumMeta
|
||||
|
||||
|
||||
class StrEnumMeta(EnumMeta):
|
||||
"""A metaclass for creating a hybrid between a namedtuple and an Enum."""
|
||||
|
||||
def keys(cls):
|
||||
return list(cls.__members__.keys())
|
||||
|
||||
def items(cls):
|
||||
return {m: v.value for m, v in cls.__members__.items()}
|
||||
|
||||
def values(cls):
|
||||
return list(cls)
|
||||
|
||||
def __contains__(cls, member):
|
||||
"""Allow strings to match against member values."""
|
||||
if isinstance(member, str):
|
||||
return any(x == member for x in cls)
|
||||
return super().__contains__(member)
|
||||
|
||||
def __getitem__(cls, member):
|
||||
"""Allow member values to be accessed by index."""
|
||||
if isinstance(member, int):
|
||||
return list(cls)[member]
|
||||
return super().__getitem__(member).value
|
||||
|
||||
def __iter__(cls):
|
||||
for item in super().__iter__():
|
||||
yield item.value
|
||||
|
||||
|
||||
class StrEnum(str, Enum, metaclass=StrEnumMeta):
|
||||
"""
|
||||
Hybrid namedtuple and Enum.
|
||||
|
||||
Creates an iterable which can use dotted access notation,
|
||||
but which is declared in class form like an Enum.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"""Use value when cast to str."""
|
||||
return str(self.value)
|
|
@ -0,0 +1,5 @@
|
|||
from .application import *
|
||||
from .health import *
|
||||
from .index import *
|
||||
from .profile import *
|
||||
from .whoami import *
|
443
src/registrar/views/application.py
Normal file
443
src/registrar/views/application.py
Normal file
|
@ -0,0 +1,443 @@
|
|||
import logging
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from registrar.forms import application_wizard as forms
|
||||
from registrar.models import DomainApplication
|
||||
from registrar.utility import StrEnum
|
||||
from registrar.views.utility import StepsHelper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Step(StrEnum):
|
||||
"""
|
||||
Names for each page of the application wizard.
|
||||
|
||||
As with Django's own `TextChoices` class, steps will
|
||||
appear in the order they are defined. (Order matters.)
|
||||
"""
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class ApplicationWizard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
A common set of methods and configuration.
|
||||
|
||||
The registrar's domain application is several pages of "steps".
|
||||
Together, these steps constitute a "wizard".
|
||||
|
||||
This base class sets up a shared state (stored in the user's session)
|
||||
between pages of the application and provides common methods for
|
||||
processing form data.
|
||||
|
||||
Views for each step should inherit from this base class.
|
||||
|
||||
Any method not marked as internal can be overridden in a subclass,
|
||||
although not without consulting the base implementation, first.
|
||||
"""
|
||||
|
||||
# uniquely namespace the wizard in urls.py
|
||||
# (this is not seen _in_ urls, only for Django's internal naming)
|
||||
# NB: this is included here for reference. Do not change it without
|
||||
# also changing the many places it is hardcoded in the HTML templates
|
||||
URL_NAMESPACE = "application"
|
||||
# name for accessing /application/<id>/edit
|
||||
EDIT_URL_NAME = "edit-application"
|
||||
|
||||
# We need to pass our human-readable step titles as context to the templates.
|
||||
TITLES = {
|
||||
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"),
|
||||
}
|
||||
|
||||
# We can use a dictionary with step names and callables that return booleans
|
||||
# to show or hide particular steps based on the state of the process.
|
||||
WIZARD_CONDITIONS = {
|
||||
Step.ORGANIZATION_FEDERAL: lambda w: w.from_model(
|
||||
"show_organization_federal", False
|
||||
),
|
||||
Step.ORGANIZATION_ELECTION: lambda w: w.from_model(
|
||||
"show_organization_election", False
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.steps = StepsHelper(self)
|
||||
self._application = None # for caching
|
||||
|
||||
def has_pk(self):
|
||||
"""Does this wizard know about a DomainApplication database record?"""
|
||||
return "application_id" in self.storage
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
"""Namespace the wizard to avoid clashes in session variable names."""
|
||||
# this is a string literal but can be made dynamic if we'd like
|
||||
# users to have multiple applications open for editing simultaneously
|
||||
return "wizard_application"
|
||||
|
||||
@property
|
||||
def application(self) -> DomainApplication:
|
||||
"""
|
||||
Attempt to match the current wizard with a DomainApplication.
|
||||
|
||||
Will create an application if none exists.
|
||||
"""
|
||||
if self._application:
|
||||
return self._application
|
||||
|
||||
if self.has_pk():
|
||||
id = self.storage["application_id"]
|
||||
try:
|
||||
self._application = DomainApplication.objects.get(
|
||||
creator=self.request.user, # type: ignore
|
||||
pk=id,
|
||||
)
|
||||
return self._application
|
||||
except DomainApplication.DoesNotExist:
|
||||
logger.debug("Application id %s did not have a DomainApplication" % id)
|
||||
|
||||
self._application = DomainApplication.objects.create(
|
||||
creator=self.request.user, # type: ignore
|
||||
)
|
||||
self.storage["application_id"] = self._application.id
|
||||
return self._application
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
# marking session as modified on every access
|
||||
# so that updates to nested keys are always saved
|
||||
self.request.session.modified = True
|
||||
return self.request.session.setdefault(self.prefix, {})
|
||||
|
||||
@storage.setter
|
||||
def storage(self, value):
|
||||
self.request.session[self.prefix] = value
|
||||
self.request.session.modified = True
|
||||
|
||||
@storage.deleter
|
||||
def storage(self):
|
||||
del self.request.session[self.prefix]
|
||||
self.request.session.modified = True
|
||||
|
||||
def done(self):
|
||||
"""Called when the user clicks the submit button, if all forms are valid."""
|
||||
self.application.submit() # change the status to submitted
|
||||
self.application.save()
|
||||
logger.debug("Application object saved: %s", self.application.id)
|
||||
return redirect(reverse(f"{self.URL_NAMESPACE}:finished"))
|
||||
|
||||
def from_model(self, attribute: str, default, *args, **kwargs):
|
||||
"""
|
||||
Get a attribute from the database model, if it exists.
|
||||
|
||||
If it is a callable, call it with any given `args` and `kwargs`.
|
||||
|
||||
This method exists so that we can avoid needlessly creating a record
|
||||
in the database before the wizard has been saved.
|
||||
"""
|
||||
if self.has_pk():
|
||||
if hasattr(self.application, attribute):
|
||||
attr = getattr(self.application, attribute)
|
||||
if callable(attr):
|
||||
return attr(*args, **kwargs)
|
||||
else:
|
||||
return attr
|
||||
else:
|
||||
raise AttributeError("Model has no attribute %s" % str(attribute))
|
||||
else:
|
||||
return default
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""This method handles GET requests."""
|
||||
|
||||
current_url = resolve(request.path_info).url_name
|
||||
|
||||
# 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 == self.EDIT_URL_NAME and "id" in kwargs:
|
||||
self.storage["application_id"] = kwargs["id"]
|
||||
|
||||
# if accessing this class directly, redirect to the first step
|
||||
# in other words, if `ApplicationWizard` is called as view
|
||||
# directly by some redirect or url handler, we'll send users
|
||||
# to the first step in the processes; subclasses will NOT
|
||||
# be redirected. The purpose of this is to allow code to
|
||||
# send users "to the application wizard" without needing to
|
||||
# know which view is first in the list of steps.
|
||||
if self.__class__ == ApplicationWizard:
|
||||
return self.goto(self.steps.first)
|
||||
|
||||
self.steps.current = current_url
|
||||
context = self.get_context_data()
|
||||
context["forms"] = self.get_forms()
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def get_all_forms(self) -> list:
|
||||
"""Calls `get_forms` for all steps and returns a flat list."""
|
||||
nested = (self.get_forms(step=step, use_db=True) for step in self.steps)
|
||||
flattened = [form for lst in nested for form in lst]
|
||||
return flattened
|
||||
|
||||
def get_forms(self, step=None, use_post=False, use_db=False, files=None):
|
||||
"""
|
||||
This method constructs the forms for a given step.
|
||||
|
||||
The form's initial data will always be gotten from the database,
|
||||
via the form's `from_database` classmethod.
|
||||
|
||||
The form's bound data will be gotten from POST if `use_post` is True,
|
||||
and from the database if `use_db` is True (provided that record exists).
|
||||
An empty form will be provided if neither of those are true.
|
||||
"""
|
||||
|
||||
kwargs = {
|
||||
"files": files,
|
||||
"prefix": self.steps.current,
|
||||
"application": self.application, # this is a property, not an object
|
||||
}
|
||||
|
||||
if step is None:
|
||||
forms = self.forms
|
||||
else:
|
||||
url = reverse(f"{self.URL_NAMESPACE}:{step}")
|
||||
forms = resolve(url).func.view_class.forms
|
||||
|
||||
instantiated = []
|
||||
|
||||
for form in forms:
|
||||
data = form.from_database(self.application) if self.has_pk() else None
|
||||
kwargs["initial"] = data
|
||||
if use_post:
|
||||
kwargs["data"] = self.request.POST
|
||||
elif use_db:
|
||||
kwargs["data"] = data
|
||||
else:
|
||||
kwargs["data"] = None
|
||||
instantiated.append(form(**kwargs))
|
||||
|
||||
return instantiated
|
||||
|
||||
def get_context_data(self):
|
||||
"""Define context for access on all wizard pages."""
|
||||
return {
|
||||
"form_titles": self.TITLES,
|
||||
"steps": self.steps,
|
||||
# Add information about which steps should be unlocked
|
||||
"visited": self.storage.get("step_history", []),
|
||||
}
|
||||
|
||||
def get_step_list(self) -> list:
|
||||
"""Dynamically generated list of steps in the form wizard."""
|
||||
step_list = []
|
||||
for step in Step:
|
||||
condition = self.WIZARD_CONDITIONS.get(step, True)
|
||||
if callable(condition):
|
||||
condition = condition(self)
|
||||
if condition:
|
||||
step_list.append(step)
|
||||
return step_list
|
||||
|
||||
def goto(self, step):
|
||||
self.steps.current = step
|
||||
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}"))
|
||||
|
||||
def goto_next_step(self):
|
||||
"""Redirects to the next step."""
|
||||
next = self.steps.next
|
||||
if next:
|
||||
self.steps.current = next
|
||||
return self.goto(next)
|
||||
else:
|
||||
raise Http404()
|
||||
|
||||
def is_valid(self, forms: list = None) -> bool:
|
||||
"""Returns True if all forms in the wizard are valid."""
|
||||
forms = forms if forms is not None else self.get_all_forms()
|
||||
are_valid = (form.is_valid() for form in forms)
|
||||
return all(are_valid)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""This method handles POST requests."""
|
||||
# if accessing this class directly, redirect to the first step
|
||||
if self.__class__ == ApplicationWizard:
|
||||
return self.goto(self.steps.first)
|
||||
|
||||
forms = self.get_forms(use_post=True)
|
||||
if self.is_valid(forms):
|
||||
# always save progress
|
||||
self.save(forms)
|
||||
else:
|
||||
# unless there are errors
|
||||
context = self.get_context_data()
|
||||
context["forms"] = forms
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
# if user opted to save their progress,
|
||||
# return them to the page they were already on
|
||||
button = request.POST.get("submit_button", None)
|
||||
if button == "save":
|
||||
return self.goto(self.steps.current)
|
||||
# otherwise, proceed as normal
|
||||
return self.goto_next_step()
|
||||
|
||||
def save(self, forms: list):
|
||||
"""
|
||||
Unpack the form responses onto the model object properties.
|
||||
|
||||
Saves the application to the database.
|
||||
"""
|
||||
for form in forms:
|
||||
if form is not None and hasattr(form, "to_database"):
|
||||
form.to_database(self.application)
|
||||
|
||||
|
||||
class OrganizationType(ApplicationWizard):
|
||||
template_name = "application_org_type.html"
|
||||
forms = [forms.OrganizationTypeForm]
|
||||
|
||||
|
||||
class OrganizationFederal(ApplicationWizard):
|
||||
template_name = "application_org_federal.html"
|
||||
forms = [forms.OrganizationFederalForm]
|
||||
|
||||
|
||||
class OrganizationElection(ApplicationWizard):
|
||||
template_name = "application_org_election.html"
|
||||
forms = [forms.OrganizationElectionForm]
|
||||
|
||||
|
||||
class OrganizationContact(ApplicationWizard):
|
||||
template_name = "application_org_contact.html"
|
||||
forms = [forms.OrganizationContactForm]
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["is_federal"] = self.application.is_federal()
|
||||
return context
|
||||
|
||||
|
||||
class AuthorizingOfficial(ApplicationWizard):
|
||||
template_name = "application_authorizing_official.html"
|
||||
forms = [forms.AuthorizingOfficialForm]
|
||||
|
||||
|
||||
class CurrentSites(ApplicationWizard):
|
||||
template_name = "application_current_sites.html"
|
||||
forms = [forms.CurrentSitesForm]
|
||||
|
||||
|
||||
class DotgovDomain(ApplicationWizard):
|
||||
template_name = "application_dotgov_domain.html"
|
||||
forms = [forms.DotGovDomainForm]
|
||||
|
||||
|
||||
class Purpose(ApplicationWizard):
|
||||
template_name = "application_purpose.html"
|
||||
forms = [forms.PurposeForm]
|
||||
|
||||
|
||||
class YourContact(ApplicationWizard):
|
||||
template_name = "application_your_contact.html"
|
||||
forms = [forms.YourContactForm]
|
||||
|
||||
|
||||
class OtherContacts(ApplicationWizard):
|
||||
template_name = "application_other_contacts.html"
|
||||
forms = [forms.OtherContactsForm]
|
||||
|
||||
|
||||
class SecurityEmail(ApplicationWizard):
|
||||
template_name = "application_security_email.html"
|
||||
forms = [forms.SecurityEmailForm]
|
||||
|
||||
|
||||
class AnythingElse(ApplicationWizard):
|
||||
template_name = "application_anything_else.html"
|
||||
forms = [forms.AnythingElseForm]
|
||||
|
||||
|
||||
class Requirements(ApplicationWizard):
|
||||
template_name = "application_requirements.html"
|
||||
forms = [forms.RequirementsForm]
|
||||
|
||||
|
||||
class Review(ApplicationWizard):
|
||||
template_name = "application_review.html"
|
||||
forms = [] # type: ignore
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["Step"] = Step.__members__
|
||||
context["application"] = self.application
|
||||
return context
|
||||
|
||||
def goto_next_step(self):
|
||||
return self.done()
|
||||
# TODO: validate before saving, show errors
|
||||
# Extra info:
|
||||
#
|
||||
# Formtools used saved POST data to revalidate each form as
|
||||
# the user had entered it. This implementation (in this file) discards
|
||||
# that data and tries to instantiate the forms from the database
|
||||
# in order to perform validation.
|
||||
#
|
||||
# This must be possible in Django (after all, that is how ModelForms work),
|
||||
# but is presently not working: the form claims it is invalid,
|
||||
# even when careful checking via breakpoint() shows that the form
|
||||
# object contains valid data.
|
||||
#
|
||||
# forms = self.get_all_forms()
|
||||
# if self.is_valid(forms):
|
||||
# return self.done()
|
||||
# else:
|
||||
# # TODO: errors to let users know why this isn't working
|
||||
# return self.goto(self.steps.current)
|
||||
|
||||
|
||||
class Finished(ApplicationWizard):
|
||||
template_name = "application_done.html"
|
||||
forms = [] # type: ignore
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data()
|
||||
context["application_id"] = self.application.id
|
||||
# clean up this wizard session, because we are done with it
|
||||
del self.storage
|
||||
return render(self.request, self.template_name, context)
|
|
@ -2,7 +2,7 @@ from django.shortcuts import redirect, render
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
|
||||
from ..forms import EditProfileForm
|
||||
from registrar.forms import EditProfileForm
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
2
src/registrar/views/utility/__init__.py
Normal file
2
src/registrar/views/utility/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
from .steps_helper import StepsHelper
|
||||
from .always_404 import always_404
|
6
src/registrar/views/utility/always_404.py
Normal file
6
src/registrar/views/utility/always_404.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.http import Http404
|
||||
|
||||
|
||||
def always_404(_, reason=None):
|
||||
"""Helpful view which always returns 404."""
|
||||
raise Http404(reason)
|
136
src/registrar/views/utility/steps_helper.py
Normal file
136
src/registrar/views/utility/steps_helper.py
Normal file
|
@ -0,0 +1,136 @@
|
|||
import logging
|
||||
from django.urls import resolve
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StepsHelper:
|
||||
"""
|
||||
Helps with form steps in a form wizard.
|
||||
|
||||
Code adapted from
|
||||
https://github.com/jazzband/django-formtools/blob/master/formtools/wizard/views.py
|
||||
|
||||
LICENSE FOR THIS CLASS
|
||||
|
||||
Copyright (c) Django Software Foundation and individual contributors.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of django-formtools nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
def __init__(self, wizard):
|
||||
self._wizard = wizard
|
||||
|
||||
def __dir__(self):
|
||||
return self.all
|
||||
|
||||
def __len__(self):
|
||||
return self.count
|
||||
|
||||
def __repr__(self):
|
||||
return "<StepsHelper for %s (steps: %s)>" % (self._wizard, self.all)
|
||||
|
||||
def __getitem__(self, step):
|
||||
return self.all[step]
|
||||
|
||||
@property
|
||||
def all(self):
|
||||
"""Returns the names of all steps."""
|
||||
return self._wizard.get_step_list()
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
"""Returns the total number of steps/forms in this the wizard."""
|
||||
return len(self.all)
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
"""
|
||||
Returns the current step. If no current step is stored in the
|
||||
storage backend, the first step will be returned.
|
||||
"""
|
||||
step = getattr(self._wizard.storage, "current_step", None)
|
||||
if step is None:
|
||||
current_url = resolve(self._wizard.request.path_info).url_name
|
||||
step = current_url if current_url in self.all else self.first
|
||||
self._wizard.storage["current_step"] = step
|
||||
return step
|
||||
|
||||
@current.setter
|
||||
def current(self, step: str):
|
||||
"""Sets the current step. Updates step history."""
|
||||
if step in self.all:
|
||||
self._wizard.storage["current_step"] = step
|
||||
else:
|
||||
logger.debug("Invalid step name %s given to StepHelper" % str(step))
|
||||
self._wizard.storage["current_step"] = self.first
|
||||
|
||||
# can't serialize a set, so keep list entries unique
|
||||
if step not in self.history:
|
||||
self.history.append(step)
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
"""Returns the name of the first step."""
|
||||
return self.all[0]
|
||||
|
||||
@property
|
||||
def last(self):
|
||||
"""Returns the name of the last step."""
|
||||
return self.all[-1]
|
||||
|
||||
@property
|
||||
def next(self):
|
||||
"""Returns the next step."""
|
||||
steps = self.all
|
||||
index = steps.index(self.current) + 1
|
||||
if index < self.count:
|
||||
return steps[index]
|
||||
return None
|
||||
|
||||
@property
|
||||
def prev(self):
|
||||
"""Returns the previous step."""
|
||||
steps = self.all
|
||||
key = steps.index(self.current) - 1
|
||||
if key >= 0:
|
||||
return steps[key]
|
||||
return None
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
"""Returns the index for the current step."""
|
||||
steps = self.all
|
||||
if self.current in steps:
|
||||
return steps.index(self)
|
||||
return None
|
||||
|
||||
@property
|
||||
def history(self):
|
||||
"""Returns the list of already visited steps."""
|
||||
return self._wizard.storage.setdefault("step_history", [])
|
|
@ -49,6 +49,8 @@
|
|||
10038 OUTOFSCOPE http://app:8080/public/css/.*
|
||||
10038 OUTOFSCOPE http://app:8080/public/js/.*
|
||||
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO)
|
||||
# This URL always returns 404, so include it as well.
|
||||
10038 OUTOFSCOPE http://app:8080/todo
|
||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||
10038 OUTOFSCOPE http://app:8080/openid/login/
|
||||
10039 FAIL (X-Backend-Server Header Information Leak - Passive/beta)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue