Merge pull request #2281 from cisagov/dk/1919-force-user-profile

Issue #1919 : force user profile - [DK]
This commit is contained in:
Rachid Mrad 2024-06-17 17:34:21 -04:00 committed by GitHub
commit 15daf6f53b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 244 additions and 17 deletions

View file

@ -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
*/ */

View file

@ -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%);
} }

View file

@ -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."}

View file

@ -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

View file

@ -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">
@ -36,6 +41,61 @@ Edit your User Profile |
</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 %}
{% block content_bottom %} {% block content_bottom %}
@ -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 %}

View file

@ -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, "Youre about to start your .gov domain request") self.assertContains(response, "Youre 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"""

View file

@ -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,6 +75,10 @@ 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"] = False
if hasattr(self.user, "finished_setup") and self.user.finished_setup:
context["user_finished_setup"] = True
context["show_back_button"] = 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