diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index ba3c37e1e..eedf5839b 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -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: diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 197667e2a..441d5239e 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -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() diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f649f9c8a..e620fff5e 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -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//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 = '" 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 = '' 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