Merge pull request #2895 from cisagov/za/2760-portfolio-domain-request-entry-point-2

#2760: Portfolio domain request entry point - [ZA]
This commit is contained in:
zandercymatics 2024-10-09 15:13:16 -06:00 committed by GitHub
commit 3db0388071
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 440 additions and 148 deletions

View file

@ -33,7 +33,7 @@ from registrar.views.utility.api_views import (
get_action_needed_email_for_user_json,
)
from registrar.views.domain_request import Step
from registrar.views.domain_request import Step, PortfolioDomainRequestStep
from registrar.views.transfer_user import TransferUserView
from registrar.views.utility import always_404
from api.views import available, rdap, get_current_federal, get_current_full
@ -61,6 +61,9 @@ for step, view in [
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
(Step.REQUIREMENTS, views.Requirements),
(Step.REVIEW, views.Review),
# Portfolio steps
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
]:
domain_request_urls.append(path(f"{step}/", view.as_view(), name=step))
@ -184,7 +187,12 @@ urlpatterns = [
name="export_data_type_requests",
),
path(
"domain-request/<id>/edit/",
"reports/export_data_type_requests/",
ExportDataTypeRequests.as_view(),
name="export_data_type_requests",
),
path(
"domain-request/<int:id>/edit/",
views.DomainRequestWizard.as_view(),
name=views.DomainRequestWizard.EDIT_URL_NAME,
),

View file

@ -21,6 +21,13 @@ from registrar.utility.constants import BranchChoices
logger = logging.getLogger(__name__)
class RequestingEntityForm(RegistrarForm):
organization_name = forms.CharField(
label="Organization name",
error_messages={"required": "Enter the name of your organization."},
)
class OrganizationTypeForm(RegistrarForm):
generic_org_type = forms.ChoiceField(
# use the long names in the domain request form

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

@ -12,8 +12,10 @@
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
{% if not is_federal %}In most instances, this requires including your states two-letter abbreviation.{% endif %}</p>
{% if not portfolio %}
<p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
{% endif %}
<p>Note that <strong>only federal agencies can request generic terms</strong> like
vote.gov.</p>

View file

@ -12,7 +12,11 @@
<h1>Youre about to start your .gov domain request.</h1>
<p>You dont have to complete the process in one session. You can save what you enter and come back to it when youre ready.</p>
{% if portfolio %}
<p>Well use the information you provide to verify your domain request meets our guidelines.</p>
{% else %}
<p>Well use the information you provide to verify your organizations eligibility for a .gov domain. Well also verify that the domain you request meets our guidelines.</p>
{% endif %}
<h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p>

View file

@ -0,0 +1,16 @@
{% extends 'domain_request_form.html' %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<p>🛸🛸🛸🛸 Placeholder content 🛸🛸🛸🛸</p>
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset">
<legend>
<h2>What is the name of your space vessel?</h2>
</legend>
{% input_with_errors forms.0.organization_name %}
</fieldset>
{% endblock %}

View file

@ -19,5 +19,9 @@
{% endblock %}
{% block form_fields %}
{% include "includes/request_review_steps.html" with is_editable=True %}
{% if portfolio %}
{% include "includes/portfolio_request_review_steps.html" with is_editable=True %}
{% else %}
{% include "includes/request_review_steps.html" with is_editable=True %}
{% endif %}
{% endblock %}

View file

@ -34,6 +34,7 @@
</ul>
</div>
<ul class="usa-nav__primary usa-accordion">
{% if not hide_domains %}
<li class="usa-nav__primary-item">
{% if has_any_domains_portfolio_permission %}
{% url 'domains' as url %}
@ -44,13 +45,14 @@
Domains
</a>
</li>
{% endif %}
<!-- <li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Domain groups
</a>
</li> -->
{% if has_organization_requests_flag %}
{% if has_organization_requests_flag and not hide_requests %}
<li class="usa-nav__primary-item">
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
{% if has_edit_request_portfolio_permission %}

View file

@ -8,7 +8,6 @@
{% endif %}
{% if step == Step.REQUESTING_ENTITY %}
{% if domain_request.organization_name %}
{% with title=form_titles|get_item:step value=domain_request %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
@ -54,32 +53,8 @@
{% endif %}
{% if step == Step.ADDITIONAL_DETAILS %}
{% with title=form_titles|get_item:step %}
{% if domain_request.has_additional_details %}
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
<h3 class="register-form-review-header">CISA Regional Representative</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if domain_request.cisa_representative_first_name %}
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
{% if domain_request.cisa_representative_email %}
<li>{{domain_request.cisa_representative_email}}</li>
{% endif %}
{% else %}
No
{% endif %}
</ul>
<h3 class="register-form-review-header">Anything else</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if domain_request.anything_else %}
{{domain_request.anything_else}}
{% else %}
No
{% endif %}
</ul>
{% else %}
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endif %}
{% with title=form_titles|get_item:step value=domain_request.anything_else|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
{% endwith %}
{% endif %}

View file

@ -0,0 +1,22 @@
{% extends 'domain_request_form.html' %}
{% load static field_helpers %}
{% block form_required_fields_help_text %}
{% include "includes/required_fields.html" %}
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Is there anything else youd like us to know about your domain request?</h2>
</legend>
</fieldset>
<div class="margin-top-3" id="anything-else">
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
</div>
{% endblock %}

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.REGULAR_TITLES
def tearDown(self):
super().tearDown()
@ -2741,6 +2741,66 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(review_page, "toggle-submit-domain-request")
self.assertContains(review_page, "Your request form is incomplete")
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_portfolio_user_missing_edit_permissions(self):
"""Tests that a portfolio user without edit request permissions cannot edit or add new requests"""
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
)
# This user should be forbidden from creating new domain requests
intro_page = self.app.get(reverse("domain-request:"), expect_errors=True)
self.assertEqual(intro_page.status_code, 403)
# This user should also be forbidden from editing existing ones
domain_request = completed_domain_request(user=self.user)
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
self.assertEqual(edit_page.status_code, 403)
# Cleanup
portfolio_perm.delete()
portfolio.delete()
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_portfolio_user_with_edit_permissions(self):
"""Tests that a portfolio user with edit request permissions can edit and add new requests"""
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
)
# This user should be allowed to create new domain requests
intro_page = self.app.get(reverse("domain-request:"))
self.assertEqual(intro_page.status_code, 200)
# This user should also be allowed to edit existing ones
domain_request = completed_domain_request(user=self.user)
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
self.assertEqual(edit_page.status_code, 200)
# Cleanup
DomainRequest.objects.all().delete()
portfolio_perm.delete()
portfolio.delete()
def test_non_creator_access(self):
"""Tests that a user cannot edit a domain request they didn't create"""
p = "password"
other_user = User.objects.create_user(username="other_user", password=p)
domain_request = completed_domain_request(user=other_user)
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}), expect_errors=True)
self.assertEqual(edit_page.status_code, 403)
def test_creator_access(self):
"""Tests that a user can edit a domain request they created"""
domain_request = completed_domain_request(user=self.user)
edit_page = self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk})).follow()
self.assertEqual(edit_page.status_code, 200)
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
def setUp(self):
@ -2903,7 +2963,7 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
self.assertNotContains(home_page, "city.gov")
class TestWizardUnlockingSteps(TestWithUser, WebTest):
class TestDomainRequestWizard(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
@ -3025,6 +3085,94 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_wizard_steps_portfolio(self):
"""
Tests the behavior of the domain request wizard for portfolios.
Ensures that:
- The user can access the organization page.
- The expected number of steps are locked/unlocked (implicit test for expected steps).
- The user lands on the "Requesting entity" page
- The user does not see the Domain and Domain requests buttons
"""
# This should unlock 4 steps by default.
# Purpose, .gov domain, current websites, and requirements for operating
domain_request = completed_domain_request(
status=DomainRequest.DomainRequestStatus.STARTED,
user=self.user,
)
domain_request.anything_else = None
domain_request.save()
federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency")
# Add a portfolio
portfolio = Portfolio.objects.create(
creator=self.user,
organization_name="test portfolio",
federal_agency=federal_agency,
)
user_portfolio_permission = UserPortfolioPermission.objects.create(
user=self.user,
portfolio=portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
)
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
# 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
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Check if the response is a redirect
if response.status_code == 302:
# Follow the redirect manually
try:
detail_page = response.follow()
self.wizard.get_context_data()
except Exception as err:
# Handle any potential errors while following the redirect
self.fail(f"Error following the redirect {err}")
# Now 'detail_page' contains the response after following the redirect
self.assertEqual(detail_page.status_code, 200)
# Assert that we're on the organization page
self.assertContains(detail_page, portfolio.organization_name)
# We should only see one unlocked step
self.assertContains(detail_page, "#check_circle", count=4)
# One pages should still be locked (additional details)
self.assertContains(detail_page, "#lock", 1)
# The current option should be selected
self.assertContains(detail_page, "usa-current", count=1)
# We default to the requesting entity page
expected_url = reverse("domain-request:portfolio_requesting_entity")
# This returns the entire url, thus "in"
self.assertIn(expected_url, detail_page.request.url)
# We shouldn't show the "domains" and "domain requests" buttons
# on this page.
self.assertNotContains(detail_page, "Domains")
self.assertNotContains(detail_page, "Domain requests")
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
# Data cleanup
user_portfolio_permission.delete()
portfolio.delete()
federal_agency.delete()
domain_request.delete()
class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
@ -3036,7 +3184,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.REGULAR_TITLES
def tearDown(self):
super().tearDown()

View file

@ -74,10 +74,13 @@ class PortfolioDomainRequestStep(StrEnum):
appear in the order they are defined. (Order matters.)
"""
# Portfolio
REQUESTING_ENTITY = "organization_name"
# NOTE: Append portfolio_ when customizing a view for portfolio.
# By default, these will redirect to the normal request flow views.
# After creating a new view, you will need to add this to urls.py.
REQUESTING_ENTITY = "portfolio_requesting_entity"
CURRENT_SITES = "current_sites"
DOTGOV_DOMAIN = "dotgov_domain"
PURPOSE = "purpose"
ADDITIONAL_DETAILS = "additional_details"
ADDITIONAL_DETAILS = "portfolio_additional_details"
REQUIREMENTS = "requirements"
REVIEW = "review"

View file

@ -43,8 +43,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
although not without consulting the base implementation, first.
"""
StepEnum: Step = Step # type: ignore
template_name = ""
is_portfolio = False
# uniquely namespace the wizard in urls.py
# (this is not seen _in_ urls, only for Django's internal naming)
@ -54,42 +54,140 @@ 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 = {} # type: ignore
# 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.titles = {}
self.wizard_conditions = {}
self.unlocking_steps = {}
self.steps = None
# Configure titles, wizard_conditions, unlocking_steps, and steps
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."""
@ -128,10 +226,16 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# If a user is creating a request, we assume that perms are handled upstream
if self.request.user.is_org_user(self.request):
portfolio = self.request.session.get("portfolio")
self._domain_request = DomainRequest.objects.create(
creator=self.request.user,
portfolio=self.request.session.get("portfolio"),
portfolio=portfolio,
)
# Question for reviewers: we should probably be doing this right?
if portfolio and not self._domain_request.generic_org_type:
self._domain_request.generic_org_type = portfolio.organization_type
self._domain_request.save()
else:
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
@ -190,6 +294,12 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
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.is_portfolio = True
# Configure titles, wizard_conditions, unlocking_steps, and steps
self.configure_step_options()
current_url = resolve(request.path_info).url_name
# if user visited via an "edit" url, associate the id of the
@ -209,7 +319,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# Clear context so the prop getter won't create a request here.
# Creating a request will be handled in the post method for the
# intro page.
return render(request, "domain_request_intro.html", {})
return render(request, "domain_request_intro.html", {"hide_requests": True, "hide_domains": True})
else:
return self.goto(self.steps.first)
@ -321,56 +431,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.
history_dict = {
"generic_org_type": self.domain_request.generic_org_type is not None,
"tribal_government": self.domain_request.tribe_name is not None,
"organization_federal": self.domain_request.federal_type is not None,
"organization_election": self.domain_request.is_election_board is not None,
"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
),
"about_your_organization": self.domain_request.about_your_organization is not None,
"senior_official": self.domain_request.senior_official is not None,
"current_sites": (
self.domain_request.current_websites.exists() or self.domain_request.requested_domain is not None
),
"dotgov_domain": self.domain_request.requested_domain is not None,
"purpose": self.domain_request.purpose is not None,
"other_contacts": (
self.domain_request.other_contacts.exists()
or self.domain_request.no_other_contacts_rationale is not None
),
"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
)
),
"requirements": self.domain_request.is_policy_acknowledged is not None,
"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."""
@ -380,11 +443,16 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
requested_domain_name = self.domain_request.requested_domain.name
context_stuff = {}
if DomainRequest._form_complete(self.domain_request, self.request):
# Note: we will want to consolidate the non_org_steps_complete check into the same check that
# org_steps_complete is using at some point.
non_org_steps_complete = DomainRequest._form_complete(self.domain_request, self.request)
org_steps_complete = len(self.db_check_for_unlocking_steps()) == len(self.steps)
if (not self.is_portfolio and non_org_steps_complete) or (self.is_portfolio and org_steps_complete):
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(),
@ -401,7 +469,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(),
@ -413,14 +481,19 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
"user": self.request.user,
"requested_domain__name": requested_domain_name,
}
# Hides the requests and domains buttons in the navbar
context_stuff["hide_requests"] = self.is_portfolio
context_stuff["hide_domains"] = self.is_portfolio
return context_stuff
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":
if step == "generic_org_type" or step == "portfolio_requesting_entity":
# We need to avoid creating a new domain request if the user
# clicks the back button
self.request.session["new_request"] = False
@ -443,21 +516,21 @@ 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.is_portfolio = True
# Configure titles, wizard_conditions, unlocking_steps, and steps
self.configure_step_options()
# which button did the user press?
button: str = request.POST.get("submit_button", "")
# If a user hits the new request url directly
if "new_request" not in request.session:
request.session["new_request"] = True
# if user has acknowledged the intro message
if button == "intro_acknowledge":
if request.path_info == self.NEW_URL_NAME:
if self.request.session["new_request"] is True:
# This will trigger the domain_request getter into creating a new DomainRequest
del self.storage
return self.goto(self.steps.first)
# Split into a function: C901 'DomainRequestWizard.post' is too complex (11)
self.handle_intro_acknowledge(request)
# if accessing this class directly, redirect to the first step
if self.__class__ == DomainRequestWizard:
@ -488,6 +561,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# otherwise, proceed as normal
return self.goto_next_step()
def handle_intro_acknowledge(self, request):
"""If we are starting a new request, clear storage
and redirect to the first step"""
if request.path_info == self.NEW_URL_NAME:
if self.request.session["new_request"] is True:
del self.storage
return self.goto(self.steps.first)
def save(self, forms: list):
"""
Unpack the form responses onto the model object properties.
@ -501,24 +582,22 @@ 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 = {
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"),
}
def __init__(self):
super().__init__()
self.steps = StepsHelper(self)
self._domain_request = None # for caching
is_portfolio = True
# Portfolio pages
class RequestingEntity(DomainRequestWizard):
template_name = "domain_request_requesting_entity.html"
forms = [forms.RequestingEntityForm]
class PortfolioAdditionalDetails(DomainRequestWizard):
template_name = "portfolio_domain_request_additional_details.html"
forms = [forms.AnythingElseForm]
# Non-portfolio pages
class OrganizationType(DomainRequestWizard):
template_name = "domain_request_org_type.html"
forms = [forms.OrganizationTypeForm]
@ -698,7 +777,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"] = Step.__members__
context["Step"] = self.get_step_enum().__members__
context["domain_request"] = self.domain_request
return context
@ -899,9 +978,9 @@ 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["form_titles"] = wizard.TITLES
context["Step"] = PortfolioDomainRequestStep.__members__
context["steps"] = request_step_list(wizard, PortfolioDomainRequestStep)
context["form_titles"] = wizard.titles
return context

View file

@ -384,10 +384,32 @@ class DomainRequestWizardPermission(PermissionsLoginMixin):
The user is in self.request.user
"""
if not self.request.user.is_authenticated:
return False
# The user has an ineligible flag
if self.request.user.is_restricted():
return False
# If the user is an org user and doesn't have add/edit perms, forbid this
if self.request.user.is_org_user(self.request):
portfolio = self.request.session.get("portfolio")
if not self.request.user.has_edit_request_portfolio_permission(portfolio):
return False
# user needs to be the creator of the domain request to edit it.
id = self.kwargs.get("id") if hasattr(self, "kwargs") else None
if not id:
domain_request_wizard = self.request.session.get("wizard_domain_request")
if domain_request_wizard:
id = domain_request_wizard.get("domain_request_id")
# If no id is provided, we can assume that the user is starting a new request.
# If one IS provided, check that they are the original creator of it.
if id:
if not DomainRequest.objects.filter(creator=self.request.user, id=id).exists():
return False
return True