mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-03 00:12:16 +02:00
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:
commit
3db0388071
14 changed files with 440 additions and 148 deletions
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 state’s two-letter abbreviation.{% endif %}</p>
|
||||
|
||||
|
||||
{% if not portfolio %}
|
||||
<p>Requests for your organization’s 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>
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
|
||||
<h1>You’re about to start your .gov domain request.</h1>
|
||||
<p>You don’t have to complete the process in one session. You can save what you enter and come back to it when you’re ready.</p>
|
||||
{% if portfolio %}
|
||||
<p>We’ll use the information you provide to verify your domain request meets our guidelines.</p>
|
||||
{% else %}
|
||||
<p>We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll 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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 you’d 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 %}
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue