Be explicit when swapping between enum / portfolio types

Centralizes all config options at the start of the class
This commit is contained in:
zandercymatics 2024-10-04 15:13:21 -06:00
parent 9a2e031979
commit 221e236775
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
3 changed files with 138 additions and 124 deletions

View file

@ -279,11 +279,11 @@ class BaseYesNoForm(RegistrarForm):
return initial_value
def request_step_list(request_wizard):
def request_step_list(request_wizard, step_enum):
"""Dynamically generated list of steps in the form wizard."""
step_list = []
for step in request_wizard.StepEnum:
condition = request_wizard.WIZARD_CONDITIONS.get(step, True)
for step in step_enum:
condition = request_wizard.wizard_conditions.get(step, True)
if callable(condition):
condition = condition(request_wizard)
if condition:

View file

@ -43,7 +43,7 @@ class DomainRequestTests(TestWithUser, WebTest):
super().setUp()
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
self.app.set_user(self.user.username)
self.TITLES = DomainRequestWizard.TITLES
self.TITLES = DomainRequestWizard.titles
def tearDown(self):
super().tearDown()
@ -3186,7 +3186,7 @@ class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
super().setUp()
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
self.app.set_user(self.user.username)
self.TITLES = DomainRequestWizard.TITLES
self.TITLES = DomainRequestWizard.titles
def tearDown(self):
super().tearDown()

View file

@ -43,9 +43,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
although not without consulting the base implementation, first.
"""
StepEnum = Step # type: ignore
template_name = ""
is_portfolio = False
# uniquely namespace the wizard in urls.py
@ -56,42 +54,136 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# name for accessing /domain-request/<id>/edit
EDIT_URL_NAME = "edit-domain-request"
NEW_URL_NAME = "/request/"
# region: Titles
# We need to pass our human-readable step titles as context to the templates.
TITLES = {
StepEnum.ORGANIZATION_TYPE: _("Type of organization"),
StepEnum.TRIBAL_GOVERNMENT: _("Tribal government"),
StepEnum.ORGANIZATION_FEDERAL: _("Federal government branch"),
StepEnum.ORGANIZATION_ELECTION: _("Election office"),
StepEnum.ORGANIZATION_CONTACT: _("Organization"),
StepEnum.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
StepEnum.SENIOR_OFFICIAL: _("Senior official"),
StepEnum.CURRENT_SITES: _("Current websites"),
StepEnum.DOTGOV_DOMAIN: _(".gov domain"),
StepEnum.PURPOSE: _("Purpose of your domain"),
StepEnum.OTHER_CONTACTS: _("Other employees from your organization"),
StepEnum.ADDITIONAL_DETAILS: _("Additional details"),
StepEnum.REQUIREMENTS: _("Requirements for operating a .gov domain"),
StepEnum.REVIEW: _("Review and submit your domain request"),
REGULAR_TITLES = {
Step.ORGANIZATION_TYPE: _("Type of organization"),
Step.TRIBAL_GOVERNMENT: _("Tribal government"),
Step.ORGANIZATION_FEDERAL: _("Federal government branch"),
Step.ORGANIZATION_ELECTION: _("Election office"),
Step.ORGANIZATION_CONTACT: _("Organization"),
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
Step.SENIOR_OFFICIAL: _("Senior official"),
Step.CURRENT_SITES: _("Current websites"),
Step.DOTGOV_DOMAIN: _(".gov domain"),
Step.PURPOSE: _("Purpose of your domain"),
Step.OTHER_CONTACTS: _("Other employees from your organization"),
Step.ADDITIONAL_DETAILS: _("Additional details"),
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
Step.REVIEW: _("Review and submit your domain request"),
}
# Titles for the portfolio context
PORTFOLIO_TITLES = {
PortfolioDomainRequestStep.REQUESTING_ENTITY: _("Requesting entity"),
PortfolioDomainRequestStep.CURRENT_SITES: _("Current websites"),
PortfolioDomainRequestStep.DOTGOV_DOMAIN: _(".gov domain"),
PortfolioDomainRequestStep.PURPOSE: _("Purpose of your domain"),
PortfolioDomainRequestStep.ADDITIONAL_DETAILS: _("Additional details"),
PortfolioDomainRequestStep.REQUIREMENTS: _("Requirements for operating a .gov domain"),
PortfolioDomainRequestStep.REVIEW: _("Review and submit your domain request"),
}
# endregion
# region: Wizard conditions
# 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 = {
StepEnum.ORGANIZATION_FEDERAL: lambda w: w.from_model("show_organization_federal", False),
StepEnum.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
StepEnum.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
StepEnum.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
REGULAR_WIZARD_CONDITIONS = {
Step.ORGANIZATION_FEDERAL: lambda w: w.from_model("show_organization_federal", False),
Step.TRIBAL_GOVERNMENT: lambda w: w.from_model("show_tribal_government", False),
Step.ORGANIZATION_ELECTION: lambda w: w.from_model("show_organization_election", False),
Step.ABOUT_YOUR_ORGANIZATION: lambda w: w.from_model("show_about_your_organization", False),
}
PORTFOLIO_WIZARD_CONDITIONS = {}
# endregion
# region: Unlocking steps
# The conditions by which each step is "unlocked" or "locked"
REGULAR_UNLOCKING_STEPS = {
Step.ORGANIZATION_TYPE: lambda self: self.domain_request.generic_org_type is not None,
Step.TRIBAL_GOVERNMENT: lambda self: self.domain_request.tribe_name is not None,
Step.ORGANIZATION_FEDERAL: lambda self: self.domain_request.federal_type is not None,
Step.ORGANIZATION_ELECTION: lambda self: self.domain_request.is_election_board is not None,
Step.ORGANIZATION_CONTACT: lambda self: (
self.domain_request.federal_agency is not None
or self.domain_request.organization_name is not None
or self.domain_request.address_line1 is not None
or self.domain_request.city is not None
or self.domain_request.state_territory is not None
or self.domain_request.zipcode is not None
or self.domain_request.urbanization is not None
),
Step.ABOUT_YOUR_ORGANIZATION: lambda self: self.domain_request.about_your_organization is not None,
Step.SENIOR_OFFICIAL: lambda self: self.domain_request.senior_official is not None,
Step.CURRENT_SITES: lambda self: (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
),
Step.DOTGOV_DOMAIN: lambda self: self.domain_request.requested_domain is not None,
Step.PURPOSE: lambda self: self.domain_request.purpose is not None,
Step.OTHER_CONTACTS: lambda self: (
self.domain_request.other_contacts.exists()
or self.domain_request.no_other_contacts_rationale is not None
),
Step.ADDITIONAL_DETAILS: lambda self: (
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
(
self.domain_request.has_anything_else_text is not None
and self.domain_request.has_cisa_representative is not None
)
),
Step.REQUIREMENTS: lambda self: self.domain_request.is_policy_acknowledged is not None,
Step.REVIEW: lambda self: self.domain_request.is_policy_acknowledged is not None,
}
PORTFOLIO_UNLOCKING_STEPS = {
PortfolioDomainRequestStep.REQUESTING_ENTITY: lambda self: self.domain_request.organization_name is not None,
PortfolioDomainRequestStep.CURRENT_SITES: lambda self: (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
),
PortfolioDomainRequestStep.DOTGOV_DOMAIN: lambda self: self.domain_request.requested_domain is not None,
PortfolioDomainRequestStep.PURPOSE: lambda self: self.domain_request.purpose is not None,
PortfolioDomainRequestStep.ADDITIONAL_DETAILS: lambda self: self.domain_request.anything_else is not None,
PortfolioDomainRequestStep.REQUIREMENTS: lambda self: self.domain_request.is_policy_acknowledged is not None,
PortfolioDomainRequestStep.REVIEW: lambda self: self.domain_request.is_policy_acknowledged is not None,
}
# endregion
def __init__(self):
super().__init__()
self.steps = StepsHelper(self)
self.configure_step_options()
self._domain_request = None # for caching
def configure_step_options(self):
"""Changes which steps are available to the user based on self.is_portfolio.
This may change on the fly, so we need to evaluate it on the fly.
Using this information, we then set three configuration variables.
- self.titles => Returns the page titles for each step
- self.wizard_conditions => Conditionally shows / hides certain steps
- self.unlocking_steps => Determines what steps are locked/unlocked
Then, we create self.steps.
"""
if self.is_portfolio:
self.titles = self.PORTFOLIO_TITLES
self.wizard_conditions = self.PORTFOLIO_WIZARD_CONDITIONS
self.unlocking_steps = self.PORTFOLIO_UNLOCKING_STEPS
else:
self.titles = self.REGULAR_TITLES
self.wizard_conditions = self.REGULAR_WIZARD_CONDITIONS
self.unlocking_steps = self.REGULAR_UNLOCKING_STEPS
self.steps = StepsHelper(self)
def has_pk(self):
"""Does this wizard know about a DomainRequest database record?"""
return "domain_request_id" in self.storage
def get_step_enum(self):
"""Determines which step enum we should use for the wizard"""
return PortfolioDomainRequestStep if self.is_portfolio else Step
@property
def prefix(self):
"""Namespace the wizard to avoid clashes in session variable names."""
@ -196,29 +288,12 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
else:
return default
def mark_as_portfolio_wizard(self):
"""Swaps the wizard over to the "portfolio" view"""
self.is_portfolio = True
self.StepEnum = PortfolioDomainRequestStep # type: ignore
self.TITLES = {
self.StepEnum.REQUESTING_ENTITY: _("Requesting entity"),
self.StepEnum.CURRENT_SITES: _("Current websites"),
self.StepEnum.DOTGOV_DOMAIN: _(".gov domain"),
self.StepEnum.PURPOSE: _("Purpose of your domain"),
self.StepEnum.ADDITIONAL_DETAILS: _("Additional details"),
self.StepEnum.REQUIREMENTS: _("Requirements for operating a .gov domain"),
self.StepEnum.REVIEW: _("Review and submit your domain request"),
}
self.WIZARD_CONDITIONS = {}
# Regenerate the steps helper
self.steps = StepsHelper(self)
def get(self, request, *args, **kwargs):
"""This method handles GET requests."""
if not self.is_portfolio and self.request.user.is_org_user(request):
self.mark_as_portfolio_wizard()
self.is_portfolio = True
self.configure_step_options()
current_url = resolve(request.path_info).url_name
@ -351,68 +426,9 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
return DomainRequest.objects.filter(creator=self.request.user, status__in=check_statuses)
def db_check_for_unlocking_steps(self):
"""Helper for get_context_data
"""Helper for get_context_data.
Queries the DB for a domain request and returns a list of unlocked steps."""
# The way this works is as follows:
# Each step is assigned a true/false value to determine if it is
# "unlocked" or not. This dictionary of values is looped through
# at the end of this function and any step with a "true" value is
# added to a simple array that is returned at the end of this function.
# This array is eventually passed to the frontend context (eg. domain_request_sidebar.html),
# and is used to determine how steps appear in the side nav.
# It is worth noting that any step assigned "false" here will be EXCLUDED
# from the list of "unlocked" steps.
if self.is_portfolio:
history_dict = {
self.StepEnum.REQUESTING_ENTITY: self.domain_request.organization_name is not None,
self.StepEnum.CURRENT_SITES: (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
),
self.StepEnum.DOTGOV_DOMAIN: self.domain_request.requested_domain is not None,
self.StepEnum.PURPOSE: self.domain_request.purpose is not None,
self.StepEnum.ADDITIONAL_DETAILS: self.domain_request.anything_else is not None,
self.StepEnum.REQUIREMENTS: self.domain_request.is_policy_acknowledged is not None,
self.StepEnum.REVIEW: self.domain_request.is_policy_acknowledged is not None,
}
else:
history_dict = {
self.StepEnum.ORGANIZATION_TYPE: self.domain_request.generic_org_type is not None,
self.StepEnum.TRIBAL_GOVERNMENT: self.domain_request.tribe_name is not None,
self.StepEnum.ORGANIZATION_FEDERAL: self.domain_request.federal_type is not None,
self.StepEnum.ORGANIZATION_ELECTION: self.domain_request.is_election_board is not None,
self.StepEnum.ORGANIZATION_CONTACT: (
self.domain_request.federal_agency is not None
or self.domain_request.organization_name is not None
or self.domain_request.address_line1 is not None
or self.domain_request.city is not None
or self.domain_request.state_territory is not None
or self.domain_request.zipcode is not None
or self.domain_request.urbanization is not None
),
self.StepEnum.ABOUT_YOUR_ORGANIZATION: self.domain_request.about_your_organization is not None,
self.StepEnum.SENIOR_OFFICIAL: self.domain_request.senior_official is not None,
self.StepEnum.CURRENT_SITES: (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
),
self.StepEnum.DOTGOV_DOMAIN: self.domain_request.requested_domain is not None,
self.StepEnum.PURPOSE: self.domain_request.purpose is not None,
self.StepEnum.OTHER_CONTACTS: (
self.domain_request.other_contacts.exists()
or self.domain_request.no_other_contacts_rationale is not None
),
self.StepEnum.ADDITIONAL_DETAILS: (
# Additional details is complete as long as "has anything else" and "has cisa rep" are not None
(
self.domain_request.has_anything_else_text is not None
and self.domain_request.has_cisa_representative is not None
)
),
self.StepEnum.REQUIREMENTS: self.domain_request.is_policy_acknowledged is not None,
self.StepEnum.REVIEW: self.domain_request.is_policy_acknowledged is not None,
}
return [key for key, value in history_dict.items() if value]
return [key for key, is_unlocked_checker in self.unlocking_steps.items() if is_unlocked_checker(self)]
def get_context_data(self):
"""Define context for access on all wizard pages."""
@ -426,7 +442,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
context_stuff = {
"not_form": False,
"form_titles": self.TITLES,
"form_titles": self.titles,
"steps": self.steps,
"visited": self.storage.get("step_history", []),
"is_federal": self.domain_request.is_federal(),
@ -443,7 +459,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
context_stuff = {
"not_form": True,
"form_titles": self.TITLES,
"form_titles": self.titles,
"steps": self.steps,
"visited": self.storage.get("step_history", []),
"is_federal": self.domain_request.is_federal(),
@ -464,7 +480,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
def get_step_list(self) -> list:
"""Dynamically generated list of steps in the form wizard."""
return request_step_list(self)
return request_step_list(self, self.get_step_enum())
def goto(self, step):
if step == "generic_org_type" or step == "portfolio_requesting_entity":
@ -491,7 +507,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
def post(self, request, *args, **kwargs) -> HttpResponse:
"""This method handles POST requests."""
if not self.is_portfolio and self.request.user.is_org_user(request): # type: ignore
self.mark_as_portfolio_wizard()
self.is_portfolio = True
self.configure_step_options()
# which button did the user press?
button: str = request.POST.get("submit_button", "")
@ -554,16 +571,13 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# TODO - this is a WIP until the domain request experience for portfolios is complete
class PortfolioDomainRequestWizard(DomainRequestWizard):
StepEnum: PortfolioDomainRequestStep = PortfolioDomainRequestStep # type: ignore
TITLES: dict = {
StepEnum.REQUESTING_ENTITY: _("Requesting entity"),
StepEnum.CURRENT_SITES: _("Current websites"),
StepEnum.DOTGOV_DOMAIN: _(".gov domain"),
StepEnum.PURPOSE: _("Purpose of your domain"),
StepEnum.ADDITIONAL_DETAILS: _("Additional details"),
StepEnum.REQUIREMENTS: _("Requirements for operating a .gov domain"),
# Step.REVIEW: _("Review and submit your domain request"),
PortfolioDomainRequestStep.REQUESTING_ENTITY: _("Requesting entity"),
PortfolioDomainRequestStep.CURRENT_SITES: _("Current websites"),
PortfolioDomainRequestStep.DOTGOV_DOMAIN: _(".gov domain"),
PortfolioDomainRequestStep.PURPOSE: _("Purpose of your domain"),
PortfolioDomainRequestStep.ADDITIONAL_DETAILS: _("Additional details"),
PortfolioDomainRequestStep.REQUIREMENTS: _("Requirements for operating a .gov domain"),
}
def __init__(self):
@ -764,7 +778,7 @@ class Review(DomainRequestWizard):
if DomainRequest._form_complete(self.domain_request, self.request) is False:
logger.warning("User arrived at review page with an incomplete form.")
context = super().get_context_data()
context["Step"] = self.StepEnum.__members__
context["Step"] = self.get_step_enum().__members__
context["domain_request"] = self.domain_request
return context
@ -965,8 +979,8 @@ class PortfolioDomainRequestStatusViewOnly(DomainRequestPortfolioViewonlyView):
# Create a temp wizard object to grab the step list
wizard = PortfolioDomainRequestWizard()
wizard.request = self.request
context["Step"] = wizard.StepEnum.__members__
context["steps"] = request_step_list(wizard)
context["Step"] = PortfolioDomainRequestStep.__members__
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
context["form_titles"] = wizard.TITLES
return context