diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 4c2f8befc..2a531a89b 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c1f5d0bb53a9268568ecaad6de6bbc8106cc2bf3a62537611ada4c69222fb9de" + "sha256": "1668475ce39851bd84ff7be330afe9766f6823cf9095980ba3b220ced3a284f4" }, "pipfile-spec": 6, "requires": {}, @@ -125,35 +125,32 @@ }, "cryptography": { "hashes": [ - "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", - "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", - "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", - "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", - "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", - "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", - "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", - "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", - "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", - "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", - "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", - "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", - "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", - "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", - "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", - "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", - "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", - "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", - "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", - "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", - "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", - "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", - "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", - "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", - "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", - "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" + "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b", + "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f", + "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190", + "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f", + "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f", + "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb", + "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c", + "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773", + "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72", + "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8", + "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717", + "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9", + "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856", + "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96", + "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288", + "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39", + "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e", + "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce", + "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1", + "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de", + "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df", + "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf", + "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458" ], "markers": "python_version >= '3.6'", - "version": "==38.0.4" + "version": "==39.0.0" }, "defusedxml": { "hashes": [ @@ -179,19 +176,19 @@ }, "django": { "hashes": [ - "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148", - "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b" + "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763", + "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef" ], "index": "pypi", - "version": "==4.1.4" + "version": "==4.1.5" }, "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": [ @@ -216,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", @@ -563,11 +552,11 @@ }, "whitenoise": { "hashes": [ - "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2", - "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567" + "sha256:cf8ecf56d86ba1c734fdb5ef6127312e39e92ad5947fef9033dc9e43ba2777d9", + "sha256:fe0af31504ab08faa1ec7fc02845432096e40cc1b27e6a7747263d7b30fb51fa" ], "index": "pypi", - "version": "==6.2.0" + "version": "==6.3.0" } }, "develop": { @@ -631,11 +620,11 @@ }, "django": { "hashes": [ - "sha256:0b223bfa55511f950ff741983d408d78d772351284c75e9f77d2b830b6b4d148", - "sha256:d38a4e108d2386cb9637da66a82dc8d0733caede4c83c4afdbda78af4214211b" + "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763", + "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef" ], "index": "pypi", - "version": "==4.1.4" + "version": "==4.1.5" }, "django-debug-toolbar": { "hashes": [ @@ -687,11 +676,11 @@ }, "gitpython": { "hashes": [ - "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f", - "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd" + "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8", + "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882" ], "markers": "python_version >= '3.7'", - "version": "==3.1.29" + "version": "==3.1.30" }, "mccabe": { "hashes": [ @@ -770,11 +759,11 @@ }, "platformdirs": { "hashes": [ - "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca", - "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e" + "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", + "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" ], "markers": "python_version >= '3.7'", - "version": "==2.6.0" + "version": "==2.6.2" }, "pycodestyle": { "hashes": [ @@ -910,11 +899,11 @@ }, "types-requests": { "hashes": [ - "sha256:48b7c06e3dffc1b6359e1888084a2b97f41b6b63f208c571ddb02ddbc6a892e4", - "sha256:8c1b1e6a0b19522b4738063e772dcee82cee1c3646536ccc4eb96f655af2b6c6" + "sha256:0ae38633734990d019b80f5463dfa164ebd3581998ac8435f526da6fe4d598c3", + "sha256:b6a2fca8109f4fdba33052f11ed86102bddb2338519e1827387137fefc66a98b" ], "index": "pypi", - "version": "==2.28.11.6" + "version": "==2.28.11.7" }, "types-urllib3": { "hashes": [ diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index b5d1f638a..fe02d389f 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -14,34 +14,32 @@ from registrar.views.application import Step from registrar.views.utility import always_404 from api.views import available -application_urls = ( - [ - path("", views.ApplicationWizard.as_view(), name=""), - # dynamically generate the other paths - *[ - path(f"{step}/", view.as_view(), name=step) - 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), - ] - ], - path("finished/", views.Finished.as_view(), name="finished"), - ], - views.ApplicationWizard.URL_NAMESPACE, -) +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("", views.index, name="home"), @@ -55,7 +53,7 @@ urlpatterns = [ path("health/", views.health), path("edit_profile/", views.edit_profile, name="edit-profile"), path("openid/", include("djangooidc.urls")), - path("register/", include(application_urls)), + path("register/", include((application_urls, APPLICATION_NAMESPACE))), path("api/v1/available/", available, name="available"), path( "todo", diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 59af6821b..9185a26a4 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -36,7 +36,7 @@ class RegistrarForm(forms.Form): @classmethod def from_database(cls, obj: DomainApplication | Contact | None): - """Initializes this form's fields with values gotten from `obj`.""" + """Returns a dict of form field values gotten from `obj`.""" if obj is None: return {} return { @@ -99,7 +99,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) @@ -113,7 +112,6 @@ class AuthorizingOfficialForm(RegistrarForm): @classmethod def from_database(cls, obj): - """Initializes this form's fields with values gotten from `obj`.""" contact = getattr(obj, "authorizing_official", None) return super().from_database(contact) @@ -130,7 +128,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() @@ -141,7 +138,6 @@ class CurrentSitesForm(RegistrarForm): @classmethod def from_database(cls, obj): - """Initializes this form's fields with values gotten from `obj`.""" current_website = obj.current_websites.first() if current_website is not None: return {"current_site": current_website.website} @@ -157,7 +153,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( @@ -183,7 +178,6 @@ class DotGovDomainForm(RegistrarForm): @classmethod def from_database(cls, obj): - """Initializes this form's fields with values gotten from `obj`.""" values = {} requested_domain = getattr(obj, "requested_domain", None) if requested_domain is not None: @@ -209,7 +203,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) @@ -223,7 +216,6 @@ class YourContactForm(RegistrarForm): @classmethod def from_database(cls, obj): - """Initializes this form's fields with values gotten from `obj`.""" contact = getattr(obj, "submitter", None) return super().from_database(contact) @@ -240,7 +232,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() @@ -256,7 +247,6 @@ class OtherContactsForm(RegistrarForm): @classmethod def from_database(cls, obj): - """Initializes this form's fields with values gotten from `obj`.""" other_contacts = obj.other_contacts.first() return super().from_database(other_contacts) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 9215d4925..f2394015f 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -16,7 +16,12 @@ logger = logging.getLogger(__name__) class Step(StrEnum): - """Names for each page of the application wizard.""" + """ + 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" @@ -95,6 +100,10 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): 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.""" @@ -134,9 +143,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): # marking session as modified on every access # so that updates to nested keys are always saved self.request.session.modified = True - if self.prefix not in self.request.session: - self.request.session[self.prefix] = {} - return self.request.session[self.prefix] + return self.request.session.setdefault(self.prefix, {}) @storage.setter def storage(self, value): @@ -187,6 +194,12 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): 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) @@ -273,10 +286,6 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): else: raise Http404() - def has_pk(self): - """Does this wizard know about a DomainApplication database record?""" - return "application_id" in self.storage - 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() @@ -401,6 +410,18 @@ class Review(ApplicationWizard): 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()