mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-29 00:40:00 +02:00
Merge remote-tracking branch 'origin/main' into nl/2248-add-org-and-portfolio-table
This commit is contained in:
commit
1f2d63070d
16 changed files with 364 additions and 31 deletions
1
.github/workflows/deploy-sandbox.yaml
vendored
1
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -27,6 +27,7 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'cb/')
|
|| startsWith(github.head_ref, 'cb/')
|
||||||
|| startsWith(github.head_ref, 'hotgov/')
|
|| startsWith(github.head_ref, 'hotgov/')
|
||||||
|| startsWith(github.head_ref, 'litterbox/')
|
|| startsWith(github.head_ref, 'litterbox/')
|
||||||
|
|| startsWith(github.head_ref, 'ag/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- ag
|
||||||
- litterbox
|
- litterbox
|
||||||
- hotgov
|
- hotgov
|
||||||
- cb
|
- cb
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- ag
|
||||||
- litterbox
|
- litterbox
|
||||||
- hotgov
|
- hotgov
|
||||||
- cb
|
- cb
|
||||||
|
|
32
ops/manifests/manifest-ag.yaml
Normal file
32
ops/manifests/manifest-ag.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-ag
|
||||||
|
buildpacks:
|
||||||
|
- python_buildpack
|
||||||
|
path: ../../src
|
||||||
|
instances: 1
|
||||||
|
memory: 512M
|
||||||
|
stack: cflinuxfs4
|
||||||
|
timeout: 180
|
||||||
|
command: ./run.sh
|
||||||
|
health-check-type: http
|
||||||
|
health-check-http-endpoint: /health
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
env:
|
||||||
|
# Send stdout and stderr straight to the terminal without buffering
|
||||||
|
PYTHONUNBUFFERED: yup
|
||||||
|
# Tell Django where to find its configuration
|
||||||
|
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||||
|
# Tell Django where it is being hosted
|
||||||
|
DJANGO_BASE_URL: https://getgov-ag.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-ag.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-ag-database
|
|
@ -1604,6 +1604,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An IIFE that displays confirmation modal on the user profile page
|
||||||
|
*/
|
||||||
|
(function userProfileListener() {
|
||||||
|
|
||||||
|
const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal');
|
||||||
|
if (showConfirmationModalTrigger) {
|
||||||
|
showConfirmationModalTrigger.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that hooks up the edit buttons on the finish-user-setup page
|
* An IIFE that hooks up the edit buttons on the finish-user-setup page
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -70,7 +70,7 @@ body {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 0; /* No width since it's a border */
|
width: 0; /* No width since it's a border */
|
||||||
height: 50%;
|
height: 40%;
|
||||||
border-left: solid 1px color('base-light');
|
border-left: solid 1px color('base-light');
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -659,6 +659,7 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
"getgov-development.app.cloud.gov",
|
"getgov-development.app.cloud.gov",
|
||||||
|
"getgov-ag.app.cloud.gov",
|
||||||
"getgov-litterbox.app.cloud.gov",
|
"getgov-litterbox.app.cloud.gov",
|
||||||
"getgov-hotgov.app.cloud.gov",
|
"getgov-hotgov.app.cloud.gov",
|
||||||
"getgov-cb.app.cloud.gov",
|
"getgov-cb.app.cloud.gov",
|
||||||
|
|
|
@ -47,7 +47,7 @@ class UserProfileForm(forms.ModelForm):
|
||||||
self.fields["middle_name"].label = "Middle name (optional)"
|
self.fields["middle_name"].label = "Middle name (optional)"
|
||||||
self.fields["last_name"].label = "Last name / family name"
|
self.fields["last_name"].label = "Last name / family name"
|
||||||
self.fields["title"].label = "Title or role in your organization"
|
self.fields["title"].label = "Title or role in your organization"
|
||||||
self.fields["email"].label = "Organizational email"
|
self.fields["email"].label = "Organization email"
|
||||||
|
|
||||||
# Set custom error messages
|
# Set custom error messages
|
||||||
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
|
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
|
||||||
|
|
|
@ -5,6 +5,7 @@ Contains middleware used in settings.py
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from registrar.models.user import User
|
||||||
from waffle.decorators import flag_is_active
|
from waffle.decorators import flag_is_active
|
||||||
|
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
|
@ -38,10 +39,17 @@ class CheckUserProfileMiddleware:
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
self.setup_page = reverse("finish-user-profile-setup")
|
self.setup_page = reverse("finish-user-profile-setup")
|
||||||
|
self.profile_page = reverse("user-profile")
|
||||||
self.logout_page = reverse("logout")
|
self.logout_page = reverse("logout")
|
||||||
self.excluded_pages = [
|
self.regular_excluded_pages = [
|
||||||
self.setup_page,
|
self.setup_page,
|
||||||
self.logout_page,
|
self.logout_page,
|
||||||
|
"/admin",
|
||||||
|
]
|
||||||
|
self.other_excluded_pages = [
|
||||||
|
self.profile_page,
|
||||||
|
self.logout_page,
|
||||||
|
"/admin",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
|
@ -61,12 +69,15 @@ class CheckUserProfileMiddleware:
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if hasattr(request.user, "finished_setup") and not request.user.finished_setup:
|
if hasattr(request.user, "finished_setup") and not request.user.finished_setup:
|
||||||
return self._handle_setup_not_finished(request)
|
if request.user.verification_type == User.VerificationTypeChoices.REGULAR:
|
||||||
|
return self._handle_regular_user_setup_not_finished(request)
|
||||||
|
else:
|
||||||
|
return self._handle_other_user_setup_not_finished(request)
|
||||||
|
|
||||||
# Continue processing the view
|
# Continue processing the view
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _handle_setup_not_finished(self, request):
|
def _handle_regular_user_setup_not_finished(self, request):
|
||||||
"""Redirects the given user to the finish setup page.
|
"""Redirects the given user to the finish setup page.
|
||||||
|
|
||||||
We set the "redirect" query param equal to where the user wants to go.
|
We set the "redirect" query param equal to where the user wants to go.
|
||||||
|
@ -82,7 +93,7 @@ class CheckUserProfileMiddleware:
|
||||||
custom_redirect = "domain-request:" if request.path == "/request/" else None
|
custom_redirect = "domain-request:" if request.path == "/request/" else None
|
||||||
|
|
||||||
# Don't redirect on excluded pages (such as the setup page itself)
|
# Don't redirect on excluded pages (such as the setup page itself)
|
||||||
if not any(request.path.startswith(page) for page in self.excluded_pages):
|
if not any(request.path.startswith(page) for page in self.regular_excluded_pages):
|
||||||
|
|
||||||
# Preserve the original query parameters, and coerce them into a dict
|
# Preserve the original query parameters, and coerce them into a dict
|
||||||
query_params = parse_qs(request.META["QUERY_STRING"])
|
query_params = parse_qs(request.META["QUERY_STRING"])
|
||||||
|
@ -98,3 +109,13 @@ class CheckUserProfileMiddleware:
|
||||||
else:
|
else:
|
||||||
# Process the view as normal
|
# Process the view as normal
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _handle_other_user_setup_not_finished(self, request):
|
||||||
|
"""Redirects the given user to the profile page to finish setup."""
|
||||||
|
|
||||||
|
# Don't redirect on excluded pages (such as the setup page itself)
|
||||||
|
if not any(request.path.startswith(page) for page in self.other_excluded_pages):
|
||||||
|
return HttpResponseRedirect(self.profile_page)
|
||||||
|
else:
|
||||||
|
# Process the view as normal
|
||||||
|
return None
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<h1>Manage your domains</h2>
|
<h1>Manage your domains</h2>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
IMPORTANT:
|
||||||
|
If this button is added on any other page, make sure to update the
|
||||||
|
relevant view to reset request.session["new_request"] = True
|
||||||
|
{% endcomment %}
|
||||||
<p class="margin-top-4">
|
<p class="margin-top-4">
|
||||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,6 +5,11 @@ Edit your User Profile |
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% load static url_helpers %}
|
{% load static url_helpers %}
|
||||||
|
|
||||||
|
{# Disable the redirect #}
|
||||||
|
{% block logo %}
|
||||||
|
{% include "includes/gov_extended_logo.html" with logo_clickable=user_finished_setup %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||||
|
@ -35,6 +40,61 @@ Edit your User Profile |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_confirmation_modal %}
|
||||||
|
<a
|
||||||
|
href="#toggle-confirmation-modal"
|
||||||
|
class="usa-button display-none show-confirmation-modal"
|
||||||
|
aria-controls="toggle-confirmation-modal"
|
||||||
|
data-open-modal
|
||||||
|
>Open confirmation modal</a>
|
||||||
|
<div
|
||||||
|
class="usa-modal usa-modal--lg is-visible"
|
||||||
|
id="toggle-confirmation-modal"
|
||||||
|
aria-labelledby="Add contact information"
|
||||||
|
aria-describedby="Add contact information"
|
||||||
|
data-force-action
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
|
Add contact information
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
<p id="modal-1-description">
|
||||||
|
.Gov domain registrants must maintain accurate contact information in the .gov registrar.
|
||||||
|
Before you can manage your domain, we need you to add your contact information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button padding-105 text-center"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Add contact information
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
@ -43,3 +103,7 @@ Edit your User Profile |
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock content_bottom %}
|
{% endblock content_bottom %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{% include "includes/footer.html" with show_manage_your_domains=user_finished_setup %}
|
||||||
|
{% endblock footer %}
|
||||||
|
|
|
@ -63,11 +63,24 @@ class TestWithUser(MockEppLib):
|
||||||
self.user.contact.title = title
|
self.user.contact.title = title
|
||||||
self.user.contact.save()
|
self.user.contact.save()
|
||||||
|
|
||||||
username_incomplete = "test_user_incomplete"
|
username_regular_incomplete = "test_regular_user_incomplete"
|
||||||
|
username_other_incomplete = "test_other_user_incomplete"
|
||||||
first_name_2 = "Incomplete"
|
first_name_2 = "Incomplete"
|
||||||
email_2 = "unicorn@igorville.com"
|
email_2 = "unicorn@igorville.com"
|
||||||
self.incomplete_user = get_user_model().objects.create(
|
# in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
|
||||||
username=username_incomplete, first_name=first_name_2, email=email_2
|
self.incomplete_regular_user = get_user_model().objects.create(
|
||||||
|
username=username_regular_incomplete,
|
||||||
|
first_name=first_name_2,
|
||||||
|
email=email_2,
|
||||||
|
verification_type=User.VerificationTypeChoices.REGULAR,
|
||||||
|
)
|
||||||
|
# in the case below, other user is representative of GRANDFATHERED,
|
||||||
|
# VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1
|
||||||
|
self.incomplete_other_user = get_user_model().objects.create(
|
||||||
|
username=username_other_incomplete,
|
||||||
|
first_name=first_name_2,
|
||||||
|
email=email_2,
|
||||||
|
verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
@ -75,8 +88,7 @@ class TestWithUser(MockEppLib):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
self.user.delete()
|
User.objects.all().delete()
|
||||||
self.incomplete_user.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class TestEnvironmentVariablesEffects(TestCase):
|
class TestEnvironmentVariablesEffects(TestCase):
|
||||||
|
@ -526,7 +538,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_new_user_with_profile_feature_on(self):
|
def test_new_user_with_profile_feature_on(self):
|
||||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
||||||
self.app.set_user(self.incomplete_user.username)
|
self.app.set_user(self.incomplete_regular_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
# This will redirect the user to the setup page.
|
# This will redirect the user to the setup page.
|
||||||
# Follow implicity checks if our redirect is working.
|
# Follow implicity checks if our redirect is working.
|
||||||
|
@ -565,7 +577,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
||||||
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
||||||
|
|
||||||
self.app.set_user(self.incomplete_user.username)
|
self.app.set_user(self.incomplete_regular_user.username)
|
||||||
with override_flag("profile_feature", active=True):
|
with override_flag("profile_feature", active=True):
|
||||||
# This will redirect the user to the setup page
|
# This will redirect the user to the setup page
|
||||||
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
||||||
|
@ -619,6 +631,106 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
self.assertContains(response, "You’re about to start your .gov domain request")
|
self.assertContains(response, "You’re about to start your .gov domain request")
|
||||||
|
|
||||||
|
|
||||||
|
class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
|
"""A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
|
||||||
|
|
||||||
|
# csrf checks do not work well with WebTest.
|
||||||
|
# We disable them here.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.user.title = None
|
||||||
|
self.user.save()
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
|
||||||
|
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.filter(domain=self.domain).delete()
|
||||||
|
self.role.delete()
|
||||||
|
self.domain.delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
Website.objects.all().delete()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
|
def _set_session_cookie(self):
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
|
||||||
|
def _submit_form_webtest(self, form, follow=False):
|
||||||
|
page = form.submit()
|
||||||
|
self._set_session_cookie()
|
||||||
|
return page.follow() if follow else page
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_new_user_with_profile_feature_on(self):
|
||||||
|
"""Tests that a new user is redirected to the profile setup page when profile_feature is on,
|
||||||
|
and testing that the confirmation modal is present"""
|
||||||
|
self.app.set_user(self.incomplete_other_user.username)
|
||||||
|
with override_flag("profile_feature", active=True):
|
||||||
|
# This will redirect the user to the user profile page.
|
||||||
|
# Follow implicity checks if our redirect is working.
|
||||||
|
user_profile_page = self.app.get(reverse("home")).follow()
|
||||||
|
self._set_session_cookie()
|
||||||
|
|
||||||
|
# Assert that we're on the right page by testing for the modal
|
||||||
|
self.assertContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
|
|
||||||
|
user_profile_page = self._submit_form_webtest(user_profile_page.form)
|
||||||
|
|
||||||
|
self.assertEqual(user_profile_page.status_code, 200)
|
||||||
|
|
||||||
|
# Assert that modal does not appear on subsequent submits
|
||||||
|
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
|
# Assert that unique error message appears by testing the message in a specific div
|
||||||
|
html_content = user_profile_page.content.decode("utf-8")
|
||||||
|
# Normalize spaces and line breaks in the HTML content
|
||||||
|
normalized_html_content = " ".join(html_content.split())
|
||||||
|
# Expected string without extra spaces and line breaks
|
||||||
|
expected_string = "Before you can manage your domain, we need you to add contact information."
|
||||||
|
# Check for the presence of the <div> element with the specific text
|
||||||
|
self.assertIn(f'<div class="usa-alert__body"> {expected_string} </div>', normalized_html_content)
|
||||||
|
|
||||||
|
# We're missing a phone number, so the page should tell us that
|
||||||
|
self.assertContains(user_profile_page, "Enter your phone number.")
|
||||||
|
|
||||||
|
# We need to assert that links to manage your domain are not present (in both body and footer)
|
||||||
|
self.assertNotContains(user_profile_page, "Manage your domains")
|
||||||
|
# Assert the tooltip on the logo, indicating that the logo is not clickable
|
||||||
|
self.assertContains(
|
||||||
|
user_profile_page, 'title="Before you can manage your domains, we need you to add contact information."'
|
||||||
|
)
|
||||||
|
# Assert that modal does not appear on subsequent submits
|
||||||
|
self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
|
||||||
|
|
||||||
|
# Add a phone number
|
||||||
|
finish_setup_form = user_profile_page.form
|
||||||
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
|
finish_setup_form["title"] = "CEO"
|
||||||
|
finish_setup_form["last_name"] = "example"
|
||||||
|
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
||||||
|
|
||||||
|
self.assertEqual(save_page.status_code, 200)
|
||||||
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
|
|
||||||
|
# We need to assert that logo is not clickable and links to manage your domain are not present
|
||||||
|
self.assertContains(save_page, "anage your domains", count=2)
|
||||||
|
self.assertNotContains(
|
||||||
|
save_page, "Before you can manage your domains, we need you to add contact information"
|
||||||
|
)
|
||||||
|
# Assert that modal does not appear on subsequent submits
|
||||||
|
self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
|
||||||
|
|
||||||
|
# Try to navigate back to the home page.
|
||||||
|
# This is the same as clicking the back button.
|
||||||
|
completed_setup_page = self.app.get(reverse("home"))
|
||||||
|
self.assertContains(completed_setup_page, "Manage your domain")
|
||||||
|
|
||||||
|
|
||||||
class UserProfileTests(TestWithUser, WebTest):
|
class UserProfileTests(TestWithUser, WebTest):
|
||||||
"""A series of tests that target your profile functionality"""
|
"""A series of tests that target your profile functionality"""
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,58 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
self.assertContains(type_page, "You cannot submit this request yet")
|
self.assertContains(type_page, "You cannot submit this request yet")
|
||||||
|
|
||||||
|
def test_domain_request_into_acknowledgement_creates_new_request(self):
|
||||||
|
"""
|
||||||
|
We had to solve a bug where the wizard was creating 2 requests on first intro acknowledgement ('continue')
|
||||||
|
The wizard was also creating multiiple requests on 'continue' -> back button -> 'continue' etc.
|
||||||
|
|
||||||
|
This tests that the domain requests get created only when they should.
|
||||||
|
"""
|
||||||
|
# Get the intro page
|
||||||
|
self.app.get(reverse("home"))
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
intro_page = self.app.get(reverse("domain-request:"))
|
||||||
|
|
||||||
|
# Select the form
|
||||||
|
intro_form = intro_page.forms[0]
|
||||||
|
|
||||||
|
# Submit the form, this creates 1 Request
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
response = intro_form.submit(name="submit_button", value="intro_acknowledge")
|
||||||
|
|
||||||
|
# Landing on the next page used to create another 1 request
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
response.follow()
|
||||||
|
|
||||||
|
# Check if a new DomainRequest object has been created
|
||||||
|
domain_request_count = DomainRequest.objects.count()
|
||||||
|
self.assertEqual(domain_request_count, 1)
|
||||||
|
|
||||||
|
# Let's go back to intro and submit again, this should not create a new request
|
||||||
|
# This is the equivalent of a back button nav from step 1 to intro -> continue
|
||||||
|
intro_form = intro_page.forms[0]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_form = intro_form.submit(name="submit_button", value="intro_acknowledge")
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_form.follow()
|
||||||
|
domain_request_count = DomainRequest.objects.count()
|
||||||
|
self.assertEqual(domain_request_count, 1)
|
||||||
|
|
||||||
|
# Go home, which will reset the session flag for new request
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
self.app.get(reverse("home"))
|
||||||
|
|
||||||
|
# This time, clicking continue will create a new request
|
||||||
|
intro_form = intro_page.forms[0]
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
intro_result = intro_form.submit(name="submit_button", value="intro_acknowledge")
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
intro_result.follow()
|
||||||
|
domain_request_count = DomainRequest.objects.count()
|
||||||
|
self.assertEqual(domain_request_count, 2)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_request_form_submission(self):
|
def test_domain_request_form_submission(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -219,22 +219,23 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
self.storage["domain_request_id"] = kwargs["id"]
|
self.storage["domain_request_id"] = kwargs["id"]
|
||||||
self.storage["step_history"] = self.db_check_for_unlocking_steps()
|
self.storage["step_history"] = self.db_check_for_unlocking_steps()
|
||||||
|
|
||||||
# if accessing this class directly, redirect to the first step
|
# if accessing this class directly, redirect to either to an acknowledgement
|
||||||
# in other words, if `DomainRequestWizard` is called as view
|
# page or to the first step in the processes (if an edit rather than a new request);
|
||||||
# directly by some redirect or url handler, we'll send users
|
# subclasseswill NOT be redirected. The purpose of this is to allow code to
|
||||||
# either to an acknowledgement page or to the first step in
|
# send users "to the domain request wizard" without needing to know which view
|
||||||
# the processes (if an edit rather than a new request); subclasses
|
# is first in the list of steps.
|
||||||
# will NOT be redirected. The purpose of this is to allow code to
|
|
||||||
# send users "to the domain request wizard" without needing to
|
|
||||||
# know which view is first in the list of steps.
|
|
||||||
context = self.get_context_data()
|
|
||||||
if self.__class__ == DomainRequestWizard:
|
if self.__class__ == DomainRequestWizard:
|
||||||
if request.path_info == self.NEW_URL_NAME:
|
if request.path_info == self.NEW_URL_NAME:
|
||||||
context = self.get_context_data()
|
# Clear context so the prop getter won't create a request here.
|
||||||
return render(request, "domain_request_intro.html", context=context)
|
# Creating a request will be handled in the post method for the
|
||||||
|
# intro page. Only TEMPORARY context needed is has_profile_flag
|
||||||
|
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
||||||
|
context_stuff = {"has_profile_feature_flag": has_profile_flag}
|
||||||
|
return render(request, "domain_request_intro.html", context=context_stuff)
|
||||||
else:
|
else:
|
||||||
return self.goto(self.steps.first)
|
return self.goto(self.steps.first)
|
||||||
|
|
||||||
|
context = self.get_context_data()
|
||||||
self.steps.current = current_url
|
self.steps.current = current_url
|
||||||
context["forms"] = self.get_forms()
|
context["forms"] = self.get_forms()
|
||||||
|
|
||||||
|
@ -434,6 +435,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
return step_list
|
return step_list
|
||||||
|
|
||||||
def goto(self, step):
|
def goto(self, step):
|
||||||
|
if step == "generic_org_type":
|
||||||
|
# We need to avoid creating a new domain request if the user
|
||||||
|
# clicks the back button
|
||||||
|
self.request.session["new_request"] = False
|
||||||
self.steps.current = step
|
self.steps.current = step
|
||||||
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}"))
|
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}"))
|
||||||
|
|
||||||
|
@ -456,11 +461,17 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
# which button did the user press?
|
# which button did the user press?
|
||||||
button: str = request.POST.get("submit_button", "")
|
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 user has acknowledged the intro message
|
||||||
if button == "intro_acknowledge":
|
if button == "intro_acknowledge":
|
||||||
if request.path_info == self.NEW_URL_NAME:
|
if request.path_info == self.NEW_URL_NAME:
|
||||||
del self.storage
|
|
||||||
|
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)
|
return self.goto(self.steps.first)
|
||||||
|
|
||||||
# if accessing this class directly, redirect to the first step
|
# if accessing this class directly, redirect to the first step
|
||||||
|
|
|
@ -10,4 +10,7 @@ def index(request):
|
||||||
# This is a django waffle flag which toggles features based off of the "flag" table
|
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||||
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
|
||||||
|
# This controls the creation of a new domain request in the wizard
|
||||||
|
request.session["new_request"] = True
|
||||||
|
|
||||||
return render(request, "home.html", context)
|
return render(request, "home.html", context)
|
||||||
|
|
|
@ -15,6 +15,7 @@ from django.urls import NoReverseMatch, reverse
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Contact,
|
Contact,
|
||||||
)
|
)
|
||||||
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||||
from waffle.decorators import flag_is_active, waffle_flag
|
from waffle.decorators import flag_is_active, waffle_flag
|
||||||
|
@ -41,6 +42,13 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
form = self.form_class(instance=self.object)
|
form = self.form_class(instance=self.object)
|
||||||
context = self.get_context_data(object=self.object, form=form)
|
context = self.get_context_data(object=self.object, form=form)
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasattr(self.user, "finished_setup")
|
||||||
|
and not self.user.finished_setup
|
||||||
|
and self.user.verification_type != User.VerificationTypeChoices.REGULAR
|
||||||
|
):
|
||||||
|
context["show_confirmation_modal"] = True
|
||||||
|
|
||||||
return_to_request = request.GET.get("return_to_request")
|
return_to_request = request.GET.get("return_to_request")
|
||||||
if return_to_request:
|
if return_to_request:
|
||||||
context["return_to_request"] = True
|
context["return_to_request"] = True
|
||||||
|
@ -67,7 +75,11 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
|
|
||||||
# The text for the back button on this page
|
# The text for the back button on this page
|
||||||
context["profile_back_button_text"] = "Go to manage your domains"
|
context["profile_back_button_text"] = "Go to manage your domains"
|
||||||
context["show_back_button"] = True
|
context["show_back_button"] = False
|
||||||
|
|
||||||
|
if hasattr(self.user, "finished_setup") and self.user.finished_setup:
|
||||||
|
context["user_finished_setup"] = True
|
||||||
|
context["show_back_button"] = True
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -94,6 +106,12 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
else:
|
else:
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
"""If the form is invalid, conditionally display an additional error."""
|
||||||
|
if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
|
||||||
|
messages.error(self.request, "Before you can manage your domain, we need you to add contact information.")
|
||||||
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Handle successful and valid form submissions."""
|
"""Handle successful and valid form submissions."""
|
||||||
form.save()
|
form.save()
|
||||||
|
@ -105,9 +123,9 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
"""Override get_object to return the logged-in user's contact"""
|
"""Override get_object to return the logged-in user's contact"""
|
||||||
user = self.request.user # get the logged in user
|
self.user = self.request.user # get the logged in user
|
||||||
if hasattr(user, "contact"): # Check if the user has a contact instance
|
if hasattr(self.user, "contact"): # Check if the user has a contact instance
|
||||||
return user.contact
|
return self.user.contact
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue