From d268ef54b19e108bbdf8c40080fe4ca6cbe94b22 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 11:42:18 -0600 Subject: [PATCH 01/66] Basic setup stuff --- src/djangooidc/backends.py | 10 ++++-- src/djangooidc/tests/test_backends.py | 10 +++--- src/djangooidc/views.py | 13 ++++++-- .../migrations/0094_user_finished_setup.py | 18 ++++++++++ src/registrar/models/user.py | 7 ++++ src/registrar/views/domain_request.py | 9 ++++- src/registrar/views/utility/mixins.py | 33 +++++++++++++++++++ .../views/utility/permission_views.py | 23 +++++++++++-- 8 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 src/registrar/migrations/0094_user_finished_setup.py diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 41e442f2d..8bdd44698 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -21,10 +21,13 @@ class OpenIdConnectBackend(ModelBackend): """ def authenticate(self, request, **kwargs): + """Returns a tuple of (User, is_new_user)""" logger.debug("kwargs %s" % kwargs) user = None + is_new_user = True + if not kwargs or "sub" not in kwargs.keys(): - return user + return user, is_new_user UserModel = get_user_model() username = self.clean_username(kwargs["sub"]) @@ -48,6 +51,7 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) + is_new_user = created if not created: # If user exists, update existing user @@ -59,10 +63,10 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - return None + return None, is_new_user # run this callback for a each login user.on_each_login() - return user + return user, is_new_user def update_existing_user(self, user, kwargs): """ diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index c15106fa9..7b7b963ea 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -21,7 +21,7 @@ class OpenIdConnectBackendTestCase(TestCase): """Test that authenticate creates a new user if it does not find existing user""" # Ensure that the authenticate method creates a new user - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user.username, "test_user") @@ -39,7 +39,7 @@ class OpenIdConnectBackendTestCase(TestCase): existing_user = User.objects.create_user(username="test_user") # Ensure that the authenticate method updates the existing user - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -68,7 +68,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -89,7 +89,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -103,5 +103,5 @@ class OpenIdConnectBackendTestCase(TestCase): def test_authenticate_with_unknown_user(self): """Test that authenticate returns None when no kwargs are supplied""" # Ensure that the authenticate method handles the case when the user is not found - user = self.backend.authenticate(request=None, **{}) + user, _ = self.backend.authenticate(request=None, **{}) self.assertIsNone(user) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 815df4ecf..c7a8f1bba 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -85,6 +85,7 @@ def login_callback(request): """Analyze the token returned by the authentication provider (OP).""" global CLIENT try: + request.session["is_new_user"] = False # If the CLIENT is none, attempt to reinitialize before handling the request if _client_is_none(): logger.debug("OIDC client is None, attempting to initialize") @@ -97,9 +98,9 @@ def login_callback(request): # add acr_value to request.session request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) - user = authenticate(request=request, **userinfo) + user, is_new_user = authenticate(request=request, **userinfo) if user: - + should_update_user = False # Fixture users kind of exist in a superposition of verification types, # because while the system "verified" them, if they login, # we don't know how the user themselves was verified through login.gov until @@ -110,9 +111,17 @@ def login_callback(request): # Set the verification type if it doesn't already exist or if its a fixture user if not user.verification_type or is_fixture_user: user.set_user_verification_type() + should_update_user = True + + if is_new_user: + user.finished_setup = False + should_update_user = True + + if should_update_user: user.save() login(request, user) + logger.info("Successfully logged in user %s" % user) # Clear the flag if the exception is not caught diff --git a/src/registrar/migrations/0094_user_finished_setup.py b/src/registrar/migrations/0094_user_finished_setup.py new file mode 100644 index 000000000..660f950c0 --- /dev/null +++ b/src/registrar/migrations/0094_user_finished_setup.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-05-09 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0093_alter_publiccontact_unique_together"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="finished_setup", + field=models.BooleanField(default=True), + ), + ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 5e4c88f63..b3fd95eb3 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -80,6 +80,13 @@ class User(AbstractUser): help_text="The means through which this user was verified", ) + # Tracks if the user finished their profile setup or not. This is so + # we can globally enforce that new users provide additional context before proceeding. + finished_setup = models.BooleanField( + # Default to true so we don't impact existing users. We set this to false downstream. + default=True + ) + def __str__(self): # this info is pulled from Login.gov if self.first_name or self.last_name: diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f93976138..6b0ef7223 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -14,7 +14,7 @@ from registrar.models.contact import Contact from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper -from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView +from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView, ContactPermissionView from .utility import ( DomainRequestPermissionView, @@ -819,3 +819,10 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates + + +class FinishContactProfileSetupView(ContactPermissionView): + """This view forces the user into providing additional details that + we may have missed from Login.gov""" + template_name = "domain_request_your_contact.html" + forms = [forms.YourContactForm] \ No newline at end of file diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index c7083ce48..4fdba113d 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -8,6 +8,7 @@ from registrar.models import ( DomainInvitation, DomainInformation, UserDomainRole, + Contact, ) import logging @@ -324,6 +325,38 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): return True +class ContactPermission(PermissionsLoginMixin): + """Permission mixin for UserDomainRole if user + has access, otherwise 403""" + + def has_permission(self): + """Check if this user has access to this domain request. + + The user is in self.request.user and the domain needs to be looked + up from the domain's primary key in self.kwargs["pk"] + """ + + # Check if the user is authenticated + if not self.request.user.is_authenticated: + return False + + user_pk = self.kwargs["pk"] + + # Check if the user has an associated contact + associated_contacts = Contact.objects.filter(user=user_pk) + associated_contacts_length = len(associated_contacts) + + if associated_contacts_length == 0: + # This means that the user trying to access this page + # is a different user than the contact holder. + return False + elif associated_contacts_length > 1: + # TODO - change this + raise ValueError("User has multiple connected contacts") + else: + return True + + class DomainRequestPermissionWithdraw(PermissionsLoginMixin): """Permission mixin that redirects to withdraw action on domain request if user has access, otherwise 403""" diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index f2752c3b5..c626367fe 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -3,8 +3,7 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView -from registrar.models import Domain, DomainRequest, DomainInvitation -from registrar.models.user_domain_role import UserDomainRole +from registrar.models import Domain, DomainRequest, DomainInvitation, UserDomainRole, Contact from .mixins import ( DomainPermission, @@ -13,6 +12,7 @@ from .mixins import ( DomainInvitationPermission, DomainRequestWizardPermission, UserDeleteDomainRolePermission, + ContactPermission, ) import logging @@ -142,3 +142,22 @@ class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteV # variable name in template context for the model object context_object_name = "userdomainrole" + + +class ContactPermissionView(ContactPermission, DetailView, abc.ABC): + """Abstract base view for domain requests that enforces permissions + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = Contact + # variable name in template context for the model object + context_object_name = "Contact" + + # Abstract property enforces NotImplementedError on an attribute. + @property + @abc.abstractmethod + def template_name(self): + raise NotImplementedError \ No newline at end of file From dda620ee4d648777652890f5b2cb28abc185de05 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:09:23 -0600 Subject: [PATCH 02/66] Add finished setup flag --- src/djangooidc/backends.py | 12 +++++------- src/djangooidc/tests/test_backends.py | 10 +++++----- src/djangooidc/views.py | 3 ++- src/registrar/admin.py | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 8bdd44698..2de6adc3e 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -21,13 +21,11 @@ class OpenIdConnectBackend(ModelBackend): """ def authenticate(self, request, **kwargs): - """Returns a tuple of (User, is_new_user)""" logger.debug("kwargs %s" % kwargs) user = None - is_new_user = True - + request.session["is_new_user"] = True if not kwargs or "sub" not in kwargs.keys(): - return user, is_new_user + return user UserModel = get_user_model() username = self.clean_username(kwargs["sub"]) @@ -51,7 +49,7 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) - is_new_user = created + request.session["is_new_user"] = created if not created: # If user exists, update existing user @@ -63,10 +61,10 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - return None, is_new_user + return None # run this callback for a each login user.on_each_login() - return user, is_new_user + return user def update_existing_user(self, user, kwargs): """ diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index 7b7b963ea..c15106fa9 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -21,7 +21,7 @@ class OpenIdConnectBackendTestCase(TestCase): """Test that authenticate creates a new user if it does not find existing user""" # Ensure that the authenticate method creates a new user - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user.username, "test_user") @@ -39,7 +39,7 @@ class OpenIdConnectBackendTestCase(TestCase): existing_user = User.objects.create_user(username="test_user") # Ensure that the authenticate method updates the existing user - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -68,7 +68,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -89,7 +89,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -103,5 +103,5 @@ class OpenIdConnectBackendTestCase(TestCase): def test_authenticate_with_unknown_user(self): """Test that authenticate returns None when no kwargs are supplied""" # Ensure that the authenticate method handles the case when the user is not found - user, _ = self.backend.authenticate(request=None, **{}) + user = self.backend.authenticate(request=None, **{}) self.assertIsNone(user) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index c7a8f1bba..4b111f130 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -98,7 +98,8 @@ def login_callback(request): # add acr_value to request.session request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) - user, is_new_user = authenticate(request=request, **userinfo) + user = authenticate(request=request, **userinfo) + is_new_user = request.session["is_new_user"] if user: should_update_user = False # Fixture users kind of exist in a superposition of verification types, diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3eea86871..a81e5e414 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -539,7 +539,7 @@ class MyUserAdmin(BaseUserAdmin): fieldsets = ( ( None, - {"fields": ("username", "password", "status", "verification_type")}, + {"fields": ("username", "password", "status", "finished_setup", "verification_type")}, ), ("Personal Info", {"fields": ("first_name", "last_name", "email")}), ( @@ -557,7 +557,7 @@ class MyUserAdmin(BaseUserAdmin): ("Important dates", {"fields": ("last_login", "date_joined")}), ) - readonly_fields = ("verification_type",) + readonly_fields = ("verification_type", "finished_setup") # Hide Username (uuid), Groups and Permissions # Q: Now that we're using Groups and Permissions, From 2f36033eb27f9bd2383f77d33735e8e2bb8663d1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:13:01 -0600 Subject: [PATCH 03/66] User setup stuff --- src/djangooidc/backends.py | 1 + src/djangooidc/views.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 2de6adc3e..3f5c1022e 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -60,6 +60,7 @@ class OpenIdConnectBackend(ModelBackend): else: try: user = UserModel.objects.get_by_natural_key(username) + request.session["is_new_user"] = False except UserModel.DoesNotExist: return None # run this callback for a each login diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 4b111f130..0eaf28f01 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -114,7 +114,9 @@ def login_callback(request): user.set_user_verification_type() should_update_user = True - if is_new_user: + # If we're dealing with a new user and if this field isn't set already, + # Then set this to False. Otherwise, if we set the field manually it'll revert. + if is_new_user and not user.finished_setup: user.finished_setup = False should_update_user = True From 84408fce4888a9d6dec516cf7c2b97598f3351d0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:45:21 -0600 Subject: [PATCH 04/66] Add perms checks --- src/djangooidc/views.py | 4 +++- src/registrar/config/urls.py | 7 +++++++ .../templates/finish_contact_setup.html | 7 +++++++ src/registrar/views/domain_request.py | 4 ++-- src/registrar/views/utility/mixins.py | 17 +++++++++++++++-- 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/registrar/templates/finish_contact_setup.html diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 0eaf28f01..c58c3a0aa 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -129,7 +129,9 @@ def login_callback(request): # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - return redirect(request.session.get("next", "/")) + + success_redirect_url = "/" if user.finished_setup else f"/finish-user-setup/{user.id}" + return redirect(request.session.get("next", success_redirect_url)) else: raise o_e.BannedUser() except o_e.StateMismatch as nsd_err: diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 720034150..2c6942ca8 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -100,6 +100,13 @@ urlpatterns = [ name="analytics", ), path("admin/", admin.site.urls), + path( + # We embed the current user ID here, but we have a permission check + # that ensures the user is who they say they are. + "finish-user-setup/", + views.FinishContactProfileSetupView.as_view(), + name="finish-contact-profile-setup", + ), path( "domain-request//edit/", views.DomainRequestWizard.as_view(), diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html new file mode 100644 index 000000000..930eb4a23 --- /dev/null +++ b/src/registrar/templates/finish_contact_setup.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% load static url_helpers %} +{% block title %} Finish setting up your profile {% endblock %} + +{% block content %} +

TEST

+{% endblock content %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 6b0ef7223..ee97dddf9 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -824,5 +824,5 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): class FinishContactProfileSetupView(ContactPermissionView): """This view forces the user into providing additional details that we may have missed from Login.gov""" - template_name = "domain_request_your_contact.html" - forms = [forms.YourContactForm] \ No newline at end of file + template_name = "finish_contact_setup.html" + forms = [forms.YourContactForm] diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 4fdba113d..45c7c7860 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -9,6 +9,7 @@ from registrar.models import ( DomainInformation, UserDomainRole, Contact, + User, ) import logging @@ -340,10 +341,22 @@ class ContactPermission(PermissionsLoginMixin): if not self.request.user.is_authenticated: return False - user_pk = self.kwargs["pk"] + + given_user_pk = self.kwargs["pk"] + + # Grab the user in the DB to do a full object comparision, not just on ids + current_user = self.request.user + + # Check for the ids existence since we're dealing with requests + requested_user_exists = User.objects.filter(pk=given_user_pk).exists() + + # Compare the PK that was passed in to the user currently logged in + if current_user.pk != given_user_pk and requested_user_exists: + # Don't allow users to modify other users profiles + return False # Check if the user has an associated contact - associated_contacts = Contact.objects.filter(user=user_pk) + associated_contacts = Contact.objects.filter(user=current_user) associated_contacts_length = len(associated_contacts) if associated_contacts_length == 0: From 4c92011279a03dd2e6b9b5aa5fbbed4da0297a75 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 13:04:54 -0600 Subject: [PATCH 05/66] Add some middleware --- src/djangooidc/views.py | 3 +- src/registrar/config/settings.py | 3 +- src/registrar/no_cache_middleware.py | 17 ---------- src/registrar/registrar_middleware.py | 46 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 20 deletions(-) delete mode 100644 src/registrar/no_cache_middleware.py create mode 100644 src/registrar/registrar_middleware.py diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index c58c3a0aa..3716ebf19 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -130,8 +130,7 @@ def login_callback(request): # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - success_redirect_url = "/" if user.finished_setup else f"/finish-user-setup/{user.id}" - return redirect(request.session.get("next", success_redirect_url)) + return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() except o_e.StateMismatch as nsd_err: diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index bbf06b825..d0849e222 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -160,7 +160,7 @@ MIDDLEWARE = [ # django-cors-headers: listen to cors responses "corsheaders.middleware.CorsMiddleware", # custom middleware to stop caching from CloudFront - "registrar.no_cache_middleware.NoCacheMiddleware", + "registrar.registrar_middleware.NoCacheMiddleware", # serve static assets in production "whitenoise.middleware.WhiteNoiseMiddleware", # provide security enhancements to the request/response cycle @@ -186,6 +186,7 @@ MIDDLEWARE = [ "auditlog.middleware.AuditlogMiddleware", # Used for waffle feature flags "waffle.middleware.WaffleMiddleware", + "registrar.registrar_middleware.CheckUserProfileMiddleware", ] # application object used by Django’s built-in servers (e.g. `runserver`) diff --git a/src/registrar/no_cache_middleware.py b/src/registrar/no_cache_middleware.py deleted file mode 100644 index 5edfca20e..000000000 --- a/src/registrar/no_cache_middleware.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Middleware to add Cache-control: no-cache to every response. - -Used to force Cloudfront caching to leave us alone while we develop -better caching responses. -""" - - -class NoCacheMiddleware: - """Middleware to add a single header to every response.""" - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - response["Cache-Control"] = "no-cache" - return response diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py new file mode 100644 index 000000000..0054f9158 --- /dev/null +++ b/src/registrar/registrar_middleware.py @@ -0,0 +1,46 @@ +""" +Contains middleware used in settings.py +""" + +from django.urls import reverse +from django.http import HttpResponseRedirect + +class CheckUserProfileMiddleware: + """ + Checks if the current user has finished_setup = False. + If they do, redirect them to the setup page regardless of where they are in + the application. + """ + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + """Code that gets executed on each request before the view is called""" + response = self.get_response(request) + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + # Check if the user is authenticated and if the setup is not finished + if request.user.is_authenticated and not request.user.finished_setup: + # Redirect to the setup page + return HttpResponseRedirect(reverse('finish-contact-profile-setup')) + + # Continue processing the view + return None + + +class NoCacheMiddleware: + """ + Middleware to add Cache-control: no-cache to every response. + + Used to force Cloudfront caching to leave us alone while we develop + better caching responses. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response["Cache-Control"] = "no-cache" + return response \ No newline at end of file From 8b41e70840bd6de3def2fd0c91cd62c1f8a0e315 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 13:43:32 -0600 Subject: [PATCH 06/66] Fine tuning --- src/djangooidc/backends.py | 8 +++++--- src/djangooidc/views.py | 6 ++---- src/registrar/registrar_middleware.py | 18 ++++++++++++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 3f5c1022e..96b7a902a 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -23,7 +23,7 @@ class OpenIdConnectBackend(ModelBackend): def authenticate(self, request, **kwargs): logger.debug("kwargs %s" % kwargs) user = None - request.session["is_new_user"] = True + if not kwargs or "sub" not in kwargs.keys(): return user @@ -49,7 +49,9 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) - request.session["is_new_user"] = created + + if created: + request.session["is_new_user"] = True if not created: # If user exists, update existing user @@ -60,8 +62,8 @@ class OpenIdConnectBackend(ModelBackend): else: try: user = UserModel.objects.get_by_natural_key(username) - request.session["is_new_user"] = False except UserModel.DoesNotExist: + request.session["is_new_user"] = True return None # run this callback for a each login user.on_each_login() diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 3716ebf19..7b5c58527 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -99,7 +99,7 @@ def login_callback(request): request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) user = authenticate(request=request, **userinfo) - is_new_user = request.session["is_new_user"] + is_new_user = request.session.get("is_new_user", False) if user: should_update_user = False # Fixture users kind of exist in a superposition of verification types, @@ -114,9 +114,7 @@ def login_callback(request): user.set_user_verification_type() should_update_user = True - # If we're dealing with a new user and if this field isn't set already, - # Then set this to False. Otherwise, if we set the field manually it'll revert. - if is_new_user and not user.finished_setup: + if is_new_user: user.finished_setup = False should_update_user = True diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 0054f9158..064757d80 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -20,10 +20,20 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): - # Check if the user is authenticated and if the setup is not finished - if request.user.is_authenticated and not request.user.finished_setup: - # Redirect to the setup page - return HttpResponseRedirect(reverse('finish-contact-profile-setup')) + # Check if setup is not finished + finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup + if request.user.is_authenticated and not finished_setup: + setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.pk}) + logout_page = reverse("logout") + excluded_pages = [ + setup_page, + logout_page, + ] + + # Don't redirect on excluded pages (such as the setup page itself) + if not any(request.path.startswith(page) for page in excluded_pages): + # Redirect to the setup page + return HttpResponseRedirect(setup_page) # Continue processing the view return None From 75499337e056bc1e2cf4998e2d45ee195328bfa8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 15:56:18 -0600 Subject: [PATCH 07/66] Initial architecture --- src/registrar/config/urls.py | 2 +- src/registrar/fixtures_users.py | 10 +- src/registrar/forms/contact.py | 42 +++++++ src/registrar/forms/domain.py | 2 +- src/registrar/forms/domain_request_wizard.py | 37 +----- .../forms/utility/wizard_form_helper.py | 7 +- src/registrar/models/contact.py | 2 - .../templates/finish_contact_setup.html | 69 ++++++++++- src/registrar/views/__init__.py | 3 + src/registrar/views/contact.py | 107 ++++++++++++++++++ src/registrar/views/domain_request.py | 8 +- .../views/utility/permission_views.py | 10 +- 12 files changed, 238 insertions(+), 61 deletions(-) create mode 100644 src/registrar/forms/contact.py create mode 100644 src/registrar/views/contact.py diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 2c6942ca8..596e5c3d2 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -104,7 +104,7 @@ urlpatterns = [ # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. "finish-user-setup/", - views.FinishContactProfileSetupView.as_view(), + views.ContactProfileSetupView.as_view(), name="finish-contact-profile-setup", ), path( diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index c31acacfd..d87438bc9 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -126,11 +126,11 @@ class UserFixture: "last_name": "Osos-Analyst", "email": "kosos@truss.works", }, - { - "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", - "first_name": "Zander-Analyst", - "last_name": "Adkinson-Analyst", - }, + # { + # "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", + # "first_name": "Zander-Analyst", + # "last_name": "Adkinson-Analyst", + # }, { "username": "57ab5847-7789-49fe-a2f9-21d38076d699", "first_name": "Paul-Analyst", diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py new file mode 100644 index 000000000..ae8d28dc8 --- /dev/null +++ b/src/registrar/forms/contact.py @@ -0,0 +1,42 @@ +from django import forms +from phonenumber_field.modelfields import PhoneNumberField # type: ignore +from django.core.validators import MaxLengthValidator + + +class ContactForm(forms.Form): + """Form for adding or editing a contact""" + + first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter your first name / given name."}, + ) + middle_name = forms.CharField( + required=False, + label="Middle name (optional)", + ) + last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter your last name / family name."}, + ) + title = forms.CharField( + label="Title or role in your organization", + error_messages={ + "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") + }, + ) + email = forms.EmailField( + label="Email", + max_length=None, + error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + ) + phone = PhoneNumberField( + label="Phone", + error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, + ) + diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index da1462bdb..9dfd9773a 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -202,7 +202,7 @@ NameserverFormset = formset_factory( validate_max=True, ) - +# TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 9d16a30de..32c59620d 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -16,6 +16,7 @@ from registrar.forms.utility.wizard_form_helper import ( from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType +from registrar.forms import ContactForm logger = logging.getLogger(__name__) @@ -385,7 +386,7 @@ class PurposeForm(RegistrarForm): ) -class YourContactForm(RegistrarForm): +class YourContactForm(RegistrarForm, ContactForm): JOIN = "submitter" def to_database(self, obj): @@ -408,40 +409,6 @@ class YourContactForm(RegistrarForm): contact = getattr(obj, "submitter", None) return super().from_database(contact) - first_name = forms.CharField( - label="First name / given name", - error_messages={"required": "Enter your first name / given name."}, - ) - middle_name = forms.CharField( - required=False, - label="Middle name (optional)", - ) - last_name = forms.CharField( - label="Last name / family name", - error_messages={"required": "Enter your last name / family name."}, - ) - title = forms.CharField( - label="Title or role in your organization", - error_messages={ - "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") - }, - ) - email = forms.EmailField( - label="Email", - max_length=None, - error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, - validators=[ - MaxLengthValidator( - 320, - message="Response must be less than 320 characters.", - ) - ], - ) - phone = PhoneNumberField( - label="Phone", - error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, - ) - class OtherContactsYesNoForm(BaseYesNoForm): """The yes/no field for the OtherContacts form.""" diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2ae50f908..350605c1a 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -21,7 +21,12 @@ class RegistrarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object - self.domain_request = kwargs.pop("domain_request", None) + if "domain_request" in kwargs: + self.domain_request = kwargs.pop("domain_request", None) + + if "contact" in kwargs: + self.contact = kwargs.pop("contact", None) + super(RegistrarForm, self).__init__(*args, **kwargs) def to_database(self, obj: DomainRequest | Contact): diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 3ebd8bc3e..3fdfccb64 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -1,10 +1,8 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel - from phonenumber_field.modelfields import PhoneNumberField # type: ignore - class Contact(TimeStampedModel): """Contact information follows a similar pattern for each contact.""" diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 930eb4a23..c6f02f64b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -1,7 +1,72 @@ {% extends "base.html" %} -{% load static url_helpers %} +{% load static form_helpers url_helpers field_helpers %} {% block title %} Finish setting up your profile {% endblock %} {% block content %} -

TEST

+
+
+
+
+ {% include "includes/form_messages.html" %} + {% comment %} + Repurposed from domain_request_form.html + {% endcomment %} + {% for outer in forms %} + {% if outer|isformset %} + {% for inner in outer.forms %} + {% include "includes/form_errors.html" with form=inner %} + {% endfor %} + {% else %} + {% include "includes/form_errors.html" with form=outer %} + {% endif %} + {% endfor %} + +

Finish setting up your profile

+ +

+ We require that you maintain accurate contact information. + The details you provide will only be used to support the administration of .gov and won’t be made public. +

+ +

What contact information should we use to reach you?

+

+ Review the details below and update any required information. + Note that editing this information won’t affect your Login.gov account information. +

+ {# TODO: maybe remove this? #} +

Required information is marked with an asterisk (*).

+
+ {% csrf_token %} +
+ + Your contact information + + + {% input_with_errors forms.0.first_name %} + + {% input_with_errors forms.0.middle_name %} + + {% input_with_errors forms.0.last_name %} + + {% input_with_errors forms.0.title %} + + {% input_with_errors forms.0.email %} + + {% with add_class="usa-input--medium" %} + {% input_with_errors forms.0.phone %} + {% endwith %} + +
+
+ +
+ {% block form_fields %}{% endblock %} +
+
+
+
+ {% endblock content %} + diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index bd15196d4..692cfd4de 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -14,5 +14,8 @@ from .domain import ( DomainInvitationDeleteView, DomainDeleteUserView, ) +from .contact import ( + ContactProfileSetupView, +) from .health import * from .index import * diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py new file mode 100644 index 000000000..9f0e4e393 --- /dev/null +++ b/src/registrar/views/contact.py @@ -0,0 +1,107 @@ +from registrar.forms.contact import ContactForm +from registrar.views.utility.permission_views import ContactPermissionView +from django.views.generic.edit import FormMixin + + +# TODO we can and probably should generalize this at this rate. +class BaseContactView(ContactPermissionView): + def get(self, request, *args, **kwargs): + self._set_contact(request) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + # TODO - this deserves a small refactor + def _set_contact(self, request): + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ + self.session = request.session + + contact_pk = "contact:" + str(self.kwargs.get("pk")) + cached_contact = self.session.get(contact_pk) + + if cached_contact: + self.object = cached_contact + else: + self.object = self.get_object() + self._update_session_with_contact() + + def _update_session_with_contact(self): + """ + Set contact pk in the session cache + """ + domain_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object + + +class ContactFormBaseView(BaseContactView, FormMixin): + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + # Set the current contact object in cache + self._set_contact(request) + + # Get the current form and validate it + form = self.get_form() + return self.check_form(form) + + # TODO rename? + def check_form(self, form): + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) + + def form_valid(self, form): + # updates session cache with contact + self._update_session_with_contact() + + # superclass has the redirect + return super().form_valid(form) + + def form_invalid(self, form): + # updates session cache with contact + self._update_session_with_contact() + + # superclass has the redirect + return super().form_invalid(form) + + +class ContactProfileSetupView(ContactPermissionView): + """This view forces the user into providing additional details that + we may have missed from Login.gov""" + template_name = "finish_contact_setup.html" + form_class = ContactForm + + def get(self, request, *args, **kwargs): + self._get_contact(request) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def _get_contact(self, request): + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ + self.session = request.session + + contact_pk = "contact:" + str(self.kwargs.get("pk")) + cached_contact = self.session.get(contact_pk) + + if cached_contact: + self.object = cached_contact + else: + self.object = self.get_object() + self._set_session_contact_pk() + + def _set_session_contact_pk(self): + """ + Set contact pk in the session cache + """ + domain_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object + diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index ee97dddf9..b07f0d53f 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -14,7 +14,7 @@ from registrar.models.contact import Contact from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper -from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView, ContactPermissionView +from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView from .utility import ( DomainRequestPermissionView, @@ -820,9 +820,3 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates - -class FinishContactProfileSetupView(ContactPermissionView): - """This view forces the user into providing additional details that - we may have missed from Login.gov""" - template_name = "finish_contact_setup.html" - forms = [forms.YourContactForm] diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index c626367fe..5587d2d56 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -153,11 +153,7 @@ class ContactPermissionView(ContactPermission, DetailView, abc.ABC): # DetailView property for what model this is viewing model = Contact - # variable name in template context for the model object - context_object_name = "Contact" + object: Contact - # Abstract property enforces NotImplementedError on an attribute. - @property - @abc.abstractmethod - def template_name(self): - raise NotImplementedError \ No newline at end of file + # variable name in template context for the model object + context_object_name = "contact" From dd9df90fb4f9c377532bd0c9b614812b2fdbfd5b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 08:33:10 -0600 Subject: [PATCH 08/66] Infra --- src/registrar/forms/contact.py | 6 ++- src/registrar/forms/domain_request_wizard.py | 37 ++++++++++++++++++- .../forms/utility/wizard_form_helper.py | 3 -- .../templates/finish_contact_setup.html | 16 ++++---- src/registrar/views/contact.py | 6 +++ 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index ae8d28dc8..1ddc1a2a0 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -1,11 +1,15 @@ from django import forms -from phonenumber_field.modelfields import PhoneNumberField # type: ignore +from phonenumber_field.formfields import PhoneNumberField # type: ignore from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): """Form for adding or editing a contact""" + def __init__(self, *args, **kwargs): + kwargs.setdefault("label_suffix", "") + super(ContactForm, self).__init__(*args, **kwargs) + first_name = forms.CharField( label="First name / given name", error_messages={"required": "Enter your first name / given name."}, diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 32c59620d..9d16a30de 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -16,7 +16,6 @@ from registrar.forms.utility.wizard_form_helper import ( from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType -from registrar.forms import ContactForm logger = logging.getLogger(__name__) @@ -386,7 +385,7 @@ class PurposeForm(RegistrarForm): ) -class YourContactForm(RegistrarForm, ContactForm): +class YourContactForm(RegistrarForm): JOIN = "submitter" def to_database(self, obj): @@ -409,6 +408,40 @@ class YourContactForm(RegistrarForm, ContactForm): contact = getattr(obj, "submitter", None) return super().from_database(contact) + first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter your first name / given name."}, + ) + middle_name = forms.CharField( + required=False, + label="Middle name (optional)", + ) + last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter your last name / family name."}, + ) + title = forms.CharField( + label="Title or role in your organization", + error_messages={ + "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") + }, + ) + email = forms.EmailField( + label="Email", + max_length=None, + error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + ) + phone = PhoneNumberField( + label="Phone", + error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, + ) + class OtherContactsYesNoForm(BaseYesNoForm): """The yes/no field for the OtherContacts form.""" diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 350605c1a..9b8a7c4d8 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -23,9 +23,6 @@ class RegistrarForm(forms.Form): # save a reference to a domain request object if "domain_request" in kwargs: self.domain_request = kwargs.pop("domain_request", None) - - if "contact" in kwargs: - self.contact = kwargs.pop("contact", None) super(RegistrarForm, self).__init__(*args, **kwargs) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index c6f02f64b..1849889c8 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -41,20 +41,22 @@ Your contact information - - {% input_with_errors forms.0.first_name %} + {{form.first_name}} + {% comment %} + {% input_with_errors form.first_name %} - {% input_with_errors forms.0.middle_name %} + {% input_with_errors form.middle_name %} - {% input_with_errors forms.0.last_name %} + {% input_with_errors form.last_name %} - {% input_with_errors forms.0.title %} + {% input_with_errors form.title %} - {% input_with_errors forms.0.email %} + {% input_with_errors form.email %} {% with add_class="usa-input--medium" %} - {% input_with_errors forms.0.phone %} + {% input_with_errors form.phone %} {% endwith %} + {% endcomment %}
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 9f0e4e393..110ee254f 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -75,6 +75,12 @@ class ContactProfileSetupView(ContactPermissionView): template_name = "finish_contact_setup.html" form_class = ContactForm + def get_form_kwargs(self, *args, **kwargs): + """Add domain_info.organization_name instance to make a bound form.""" + form_kwargs = super().get_form_kwargs(*args, **kwargs) + form_kwargs["instance"] = self.object + return form_kwargs + def get(self, request, *args, **kwargs): self._get_contact(request) context = self.get_context_data(object=self.object) From a55f3391682b1e79d663b23d2a6dcb34e337cf91 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 09:54:38 -0600 Subject: [PATCH 09/66] Hook form to db --- src/registrar/forms/contact.py | 21 ++++++- src/registrar/registrar_middleware.py | 2 +- .../templates/finish_contact_setup.html | 4 +- src/registrar/views/contact.py | 61 +++++++------------ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 1ddc1a2a0..d699087c9 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -6,9 +6,24 @@ from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): """Form for adding or editing a contact""" - def __init__(self, *args, **kwargs): - kwargs.setdefault("label_suffix", "") - super(ContactForm, self).__init__(*args, **kwargs) + def to_database(self, obj): + """ + Adds this form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + """ + if not self.is_valid(): + return + for name, value in self.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + @classmethod + def from_database(cls, obj): + """Returns a dict of form field values gotten from `obj`.""" + if obj is None: + return {} + return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore first_name = forms.CharField( label="First name / given name", diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 064757d80..783951279 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -23,7 +23,7 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.pk}) + setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 1849889c8..1d04c6a8f 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -42,7 +42,7 @@ Your contact information {{form.first_name}} - {% comment %} + {% input_with_errors form.first_name %} {% input_with_errors form.middle_name %} @@ -56,7 +56,7 @@ {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} - {% endcomment %} +
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 110ee254f..c640c9150 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,10 +1,13 @@ +from django.urls import reverse from registrar.forms.contact import ContactForm +from registrar.models.contact import Contact from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): + def get(self, request, *args, **kwargs): self._set_contact(request) context = self.get_context_data(object=self.object) @@ -38,6 +41,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): + def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -54,13 +58,6 @@ class ContactFormBaseView(BaseContactView, FormMixin): def check_form(self, form): return self.form_valid(form) if form.is_valid() else self.form_invalid(form) - def form_valid(self, form): - # updates session cache with contact - self._update_session_with_contact() - - # superclass has the redirect - return super().form_valid(form) - def form_invalid(self, form): # updates session cache with contact self._update_session_with_contact() @@ -69,45 +66,33 @@ class ContactFormBaseView(BaseContactView, FormMixin): return super().form_invalid(form) -class ContactProfileSetupView(ContactPermissionView): +class ContactProfileSetupView(ContactFormBaseView): """This view forces the user into providing additional details that we may have missed from Login.gov""" template_name = "finish_contact_setup.html" form_class = ContactForm + model = Contact - def get_form_kwargs(self, *args, **kwargs): - """Add domain_info.organization_name instance to make a bound form.""" - form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.object - return form_kwargs + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + # TODO - some logic should exist that navigates them to the domain request page if + # they clicked it on get.gov - def get(self, request, *args, **kwargs): - self._get_contact(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) + # The user has finished their setup - def _get_contact(self, request): - """ - get domain from session cache or from db and set - to self.object - set session to self for downstream functions to - update session cache - """ - self.session = request.session - contact_pk = "contact:" + str(self.kwargs.get("pk")) - cached_contact = self.session.get(contact_pk) + # Add a notification that the update was successful + return reverse("home") - if cached_contact: - self.object = cached_contact - else: - self.object = self.get_object() - self._set_session_contact_pk() + def form_valid(self, form): + self.request.user.finished_setup = True + self.request.user.save() - def _set_session_contact_pk(self): - """ - Set contact pk in the session cache - """ - domain_pk = "contact:" + str(self.kwargs.get("pk")) - self.session[domain_pk] = self.object + form.to_database(self.object) + self._update_session_with_contact() + return super().form_valid(form) + + def get_initial(self): + """The initial value for the form (which is a formset here).""" + return self.form_class.from_database(self.object) From ebf13280444cfdd61da45835b1c9ee48fd925722 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 11:26:42 -0600 Subject: [PATCH 10/66] Template logic --- src/registrar/assets/sass/_theme/_base.scss | 17 ++++++++ src/registrar/forms/contact.py | 10 +---- .../templates/finish_contact_setup.html | 40 ++++++++++++------- .../templates/includes/input_with_errors.html | 36 +++++++++++++++-- src/registrar/templatetags/field_helpers.py | 9 +++++ 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 212df992f..fc132ebaa 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -140,3 +140,20 @@ abbr[title] { .cursor-pointer { cursor: pointer; } + +.input-with-edit-button { + svg.usa-icon { + width: 1.5em !important; + height: 1.5em !important; + // TODO CHANGE + color: green; + position: absolute; + } + &.input-with-edit-button__error { + // TODO CHANGE + svg.usa-icon { + color: red; + } + } + +} diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index d699087c9..e2a47f56c 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -44,15 +44,9 @@ class ContactForm(forms.Form): }, ) email = forms.EmailField( - label="Email", + label="Organization email", max_length=None, - error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, - validators=[ - MaxLengthValidator( - 320, - message="Response must be less than 320 characters.", - ) - ], + required=False, ) phone = PhoneNumberField( label="Phone", diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 1d04c6a8f..e825dc90b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -41,23 +41,33 @@ Your contact information - {{form.first_name}} - - {% input_with_errors form.first_name %} - - {% input_with_errors form.middle_name %} - - {% input_with_errors form.last_name %} - - {% input_with_errors form.title %} - - {% input_with_errors form.email %} - - {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone %} + + {% with show_edit_button=True %} + {% input_with_errors form.first_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.middle_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.last_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.email %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.title %} + {% endwith %} + + {% with show_edit_button=True %} + {% with add_class="usa-input--medium" %} + {% input_with_errors form.phone %} + {% endwith %} {% endwith %} -
+
+
+ {% else %} + {% include "django/forms/label.html" %} + {% endif %} {% endif %} {% if sublabel_text %} @@ -58,9 +69,26 @@ error messages, if necessary. {% if append_gov %}
{% endif %} - {# this is the input field, itself #} - {% include widget.template_name %} + {% if show_edit_button %} +
+ +
+ {{ field.value }} +
+
+ {# this is the input field, itself #} + {% include widget.template_name %} + {% else %} + {# this is the input field, itself #} + {% include widget.template_name %} + {% endif %} {% if append_gov %} .gov
diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index 811897908..a7aa9d663 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -26,6 +26,7 @@ def input_with_errors(context, field=None): # noqa: C901 add_group_class: append to input element's surrounding tag's `class` attribute attr_* - adds or replaces any single html attribute for the input add_error_attr_* - like `attr_*` but only if field.errors is not empty + show_edit_button: shows a simple edit button, and adds display-none to the input field. Example usage: ``` @@ -91,6 +92,14 @@ def input_with_errors(context, field=None): # noqa: C901 elif key == "add_group_class": group_classes.append(value) + elif key == "show_edit_button": + # Hide the primary input field. + # Used such that we can toggle it with JS + if "display-none" not in classes and isinstance(value, bool) and value: + classes.append("display-none") + # Set this as a context value so we know what we're going to display + context["show_edit_button"] = value + attrs["id"] = field.auto_id # do some work for various edge cases From 3a47a7a61c71a01ebc656789ce106b7aafef4ba0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 13:08:57 -0600 Subject: [PATCH 11/66] Refinement --- src/registrar/assets/js/get-gov.js | 32 +++++++++++++++++++ src/registrar/assets/sass/_theme/_base.scss | 12 ++++++- src/registrar/assets/sass/_theme/_forms.scss | 17 ++++++++++ src/registrar/forms/contact.py | 5 +++ src/registrar/models/contact.py | 12 +++++++ .../templates/finish_contact_setup.html | 18 +++++++---- .../templates/includes/input_with_errors.html | 11 +++++-- src/registrar/views/contact.py | 10 +++--- 8 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e7260ee21..6aaca61a9 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -834,3 +834,35 @@ function hideDeletedForms() { (function cisaRepresentativesFormListener() { HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) })(); + + + +/** + * An IIFE that hooks up the edit buttons on the finish-user-setup page + */ +(function finishUserSetupListener() { + function showInputFieldHideReadonlyField(inputField, readonlyField, editButton) { + readonlyField.classList.add('display-none'); + inputField.classList.remove('display-none'); + editButton.classList.add('display-none'); + } + + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + let fieldIdParts = button.id.split("__") + + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldId = fieldIdParts[0] + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true + + inputField = document.querySelector(`#id_${fieldId}`) + readonlyField = document.querySelector(`#${fieldId}__edit-button-readonly`) + showInputFieldHideReadonlyField(inputField, readonlyField, button) + + // Unlock after it completes + button.disabled = false + }); + } + }); +})(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index fc132ebaa..7677c8ffe 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -141,6 +141,7 @@ abbr[title] { cursor: pointer; } +// todo this class should ideally be renamed .input-with-edit-button { svg.usa-icon { width: 1.5em !important; @@ -154,6 +155,15 @@ abbr[title] { svg.usa-icon { color: red; } + div.readonly-field { + color: red + } + } +} + +.input-with-edit-button--button { + svg { + width: 1.25em !important; + height: 1.25em !important; } - } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 058a9f6c8..2766f596c 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -26,6 +26,23 @@ } } +.usa-form--edit-button-form { + margin-top: 0.5em; + // todo update + border-top: 2px black solid; + label.usa-label { + font-weight: bold; + } +} + +.usa-form--edit-button-form:first-of-type { + border-top: None +} + +.usa-form--edit-button-form > .usa-form-group:first-of-type { + margin-top: unset; +} + .usa-form-group--unstyled-error { margin-left: 0; padding-left: 0; diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index e2a47f56c..9d7d6a641 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -25,6 +25,11 @@ class ContactForm(forms.Form): return {} return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore + + full_name = forms.CharField( + label="Full name", + error_messages={"required": "Enter your full name"}, + ) first_name = forms.CharField( label="First name / given name", error_messages={"required": "Enter your first name / given name."}, diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 3fdfccb64..69d28df52 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -126,3 +126,15 @@ class Contact(TimeStampedModel): return str(self.pk) else: return "" + + @property + def full_name(self, separator=" "): + """ + Returns the full name (first_name, middle_name, last_name) of this contact. + Seperator (which defaults to a blank space) determines the seperator for each of those fields. + For instance, with seperator=", " - this function would return this: + "First, Middle, Last" + """ + # Filter out empty strings to avoid extra spaces or separators + parts = [self.first_name or "", self.middle_name or "", self.last_name or ""] + return separator.join(parts) \ No newline at end of file diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index e825dc90b..4892a64b7 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -42,27 +42,33 @@ Your contact information - {% with show_edit_button=True %} + {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} + {# Also todo: consolidate all of the scattered classes into this usa form one #} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% input_with_errors form.full_name %} + {% endwith %} + + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} {% input_with_errors form.first_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} {% input_with_errors form.middle_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none"%} {% input_with_errors form.last_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% input_with_errors form.email %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 9303fb61d..d1dc38bc3 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -32,8 +32,13 @@ error messages, if necessary.
{% include "django/forms/label.html" %}
-
- +
+
{% else %} @@ -71,7 +76,7 @@ error messages, if necessary. {% endif %} {% if show_edit_button %} -
+

Finish setting up your profile

- We require that you maintain accurate contact information. + {% public_site_url 'help/account-management/#get-help-with-login.gov' %} + We require + that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

@@ -44,31 +46,32 @@ {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} {# Also todo: consolidate all of the scattered classes into this usa form one #} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} {% input_with_errors form.first_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} {% input_with_errors form.middle_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none"%} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none"%} {% input_with_errors form.last_name %} {% endwith %} - - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + + {# TODO: I shouldnt need to do add_class here #} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index d1dc38bc3..43e90dca6 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -28,19 +28,7 @@ error messages, if necessary. {% if not field.widget_type == "checkbox" %} {% if show_edit_button %} -
-
- {% include "django/forms/label.html" %} -
-
- -
-
+ {% include "includes/label_with_edit_button.html" %} {% else %} {% include "django/forms/label.html" %} {% endif %} @@ -75,25 +63,13 @@ error messages, if necessary.
{% endif %} - {% if show_edit_button %} -
- -
- {{ field.value }} -
-
- {# this is the input field, itself #} - {% include widget.template_name %} - {% else %} - {# this is the input field, itself #} - {% include widget.template_name %} + {% if show_readonly %} + {% include "includes/readonly_input.html" %} {% endif %} + + {# this is the input field, itself #} + {% include widget.template_name %} + {% if append_gov %} .gov
diff --git a/src/registrar/templates/includes/label_with_edit_button.html b/src/registrar/templates/includes/label_with_edit_button.html new file mode 100644 index 000000000..bbff26400 --- /dev/null +++ b/src/registrar/templates/includes/label_with_edit_button.html @@ -0,0 +1,15 @@ + +{% load static field_helpers url_helpers %} +
+
+ {% include "django/forms/label.html" %} +
+
+ +
+
\ No newline at end of file diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html new file mode 100644 index 000000000..820d4b66c --- /dev/null +++ b/src/registrar/templates/includes/readonly_input.html @@ -0,0 +1,14 @@ +{% load static field_helpers url_helpers %} + +
+ +
+ {{ field.value }} +
+
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 3c269587b..f70f59a26 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -94,3 +94,11 @@ class ContactProfileSetupView(ContactFormBaseView): """The initial value for the form (which is a formset here).""" db_object = self.form_class.from_database(self.object) return db_object + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["email_sublabel_text"] = ( + "We recommend using your work email for your .gov account. " + "If the wrong email is displayed below, you’ll need to update your Login.gov account " + "and log back in. Get help with your Login.gov account.") + return context From 7ec39e7d13343b8e63383903307c5c8c814ae689 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 10:32:32 -0600 Subject: [PATCH 14/66] Match colors, and js functionality --- src/registrar/assets/js/get-gov.js | 75 +++++++++++++------ src/registrar/assets/sass/_theme/_base.scss | 9 +-- src/registrar/assets/sass/_theme/_forms.scss | 3 +- .../templates/finish_contact_setup.html | 2 +- .../templates/includes/input_with_errors.html | 2 +- .../includes/label_with_edit_button.html | 2 +- src/registrar/views/utility/mixins.py | 17 +++-- 7 files changed, 72 insertions(+), 38 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 18ab1ebce..99fd9b3a1 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -841,6 +841,8 @@ function hideDeletedForms() { * An IIFE that hooks up the edit buttons on the finish-user-setup page */ (function finishUserSetupListener() { + + // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { let inputId = getInputFieldId(fieldName) let inputField = document.querySelector(inputId) @@ -885,35 +887,60 @@ function hideDeletedForms() { } } }); - - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.remove(); - } } } - document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { - let fieldIdParts = button.id.split("__") + function handleEditButtonClick(fieldName, button){ + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true - if (fieldIdParts && fieldIdParts.length > 0){ - let fieldName = fieldIdParts[0] - button.addEventListener('click', function() { - // Lock the edit button while this operation occurs - button.disabled = true + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + showInputFieldHideReadonlyField(fieldName, button); + } - if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } + button.classList.add('display-none'); + + // Unlock after it completes + button.disabled = false + }); + } + + function setupListener(){ + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + // Get the "{field_name}" and "edit-button" + let fieldIdParts = button.id.split("__") + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldName = fieldIdParts[0] + + // When the edit button is clicked, show the input field under it + handleEditButtonClick(fieldName, button); + } + }); + } + + function showInputOnErrorFields(){ + document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + let fieldIdParts = button.id.split("__") + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldName = fieldIdParts[0] - button.classList.add('display-none'); + let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); + if (errorMessage) { + button.click() + } + } + }); + }); + } - // Unlock after it completes - button.disabled = false - }); - } - }); + // Hookup all edit buttons to the `handleEditButtonClick` function + setupListener(); + + // Show the input fields if an error exists + showInputOnErrorFields(); })(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index bfdd99fc2..beb63cdd1 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -1,4 +1,5 @@ @use "uswds-core" as *; +@use "cisa_colors" as *; /* Styles for making visible to screen reader / AT users only. */ .sr-only { @@ -146,17 +147,15 @@ abbr[title] { svg.usa-icon { width: 1.5em !important; height: 1.5em !important; - // TODO CHANGE - color: green; + color: #{$dhs-green}; position: absolute; } &.input-with-edit-button__error { - // TODO CHANGE svg.usa-icon { - color: red; + color: #{$dhs-red}; } div.readonly-field { - color: red; + color: #{$dhs-red}; } } } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index bf1f81113..a4194273d 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -1,4 +1,5 @@ @use "uswds-core" as *; +@use "cisa_colors" as *; .usa-form .usa-button { margin-top: units(3); @@ -28,7 +29,7 @@ .usa-form-readonly { // todo update - border-top: 2px black solid; + border-top: 2px #{$dhs-dark-gray-15} solid; .bold-usa-label label.usa-label{ font-weight: bold; diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 39429d4eb..b9d1ca6f6 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -7,7 +7,7 @@
- {% include "includes/form_messages.html" %} + {% include "includes/form_errors.html" with form=form %} {% comment %} Repurposed from domain_request_form.html {% endcomment %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 43e90dca6..9348713b3 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -28,7 +28,7 @@ error messages, if necessary. {% if not field.widget_type == "checkbox" %} {% if show_edit_button %} - {% include "includes/label_with_edit_button.html" %} + {% include "includes/label_with_edit_button.html" with bold_label=True %} {% else %} {% include "django/forms/label.html" %} {% endif %} diff --git a/src/registrar/templates/includes/label_with_edit_button.html b/src/registrar/templates/includes/label_with_edit_button.html index bbff26400..ab0a04ee4 100644 --- a/src/registrar/templates/includes/label_with_edit_button.html +++ b/src/registrar/templates/includes/label_with_edit_button.html @@ -1,6 +1,6 @@ {% load static field_helpers url_helpers %} -
+
{% include "django/forms/label.html" %}
diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 45c7c7860..49d172971 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -342,18 +342,25 @@ class ContactPermission(PermissionsLoginMixin): return False - given_user_pk = self.kwargs["pk"] + given_contact_pk = self.kwargs["pk"] # Grab the user in the DB to do a full object comparision, not just on ids current_user = self.request.user - # Check for the ids existence since we're dealing with requests - requested_user_exists = User.objects.filter(pk=given_user_pk).exists() - # Compare the PK that was passed in to the user currently logged in - if current_user.pk != given_user_pk and requested_user_exists: + if current_user.contact.pk != given_contact_pk: # Don't allow users to modify other users profiles return False + + # Check if the object at the id we're searching on actually exists + requested_user_exists = User.objects.filter(pk=current_user.pk).exists() + requested_contact_exists = Contact.objects.filter( + user=current_user.pk, + pk=given_contact_pk + ).exists() + + if not requested_user_exists or not requested_contact_exists: + return False # Check if the user has an associated contact associated_contacts = Contact.objects.filter(user=current_user) From 4592b0c9e67ea6ba90bdf5071f7bf6d5e839afef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 12:52:35 -0600 Subject: [PATCH 15/66] Redirect logic --- src/registrar/assets/js/get-gov.js | 17 +++-- src/registrar/forms/contact.py | 40 +++++------- src/registrar/models/contact.py | 19 +++--- .../models/utility/generic_helper.py | 20 ++++++ src/registrar/registrar_middleware.py | 4 +- .../templates/finish_contact_setup.html | 13 ++-- src/registrar/views/contact.py | 62 ++++++++++++++----- 7 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 99fd9b3a1..73afd8131 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -895,13 +895,7 @@ function hideDeletedForms() { // Lock the edit button while this operation occurs button.disabled = true - if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } - + showInputFieldHideReadonlyField(fieldName, button); button.classList.add('display-none'); // Unlock after it completes @@ -928,10 +922,15 @@ function hideDeletedForms() { let fieldIdParts = button.id.split("__") if (fieldIdParts && fieldIdParts.length > 0){ let fieldName = fieldIdParts[0] - + let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { - button.click() + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + button.click() + } } } }); diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 51c296375..83b49f548 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -1,6 +1,5 @@ from django import forms from phonenumber_field.formfields import PhoneNumberField # type: ignore -from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): @@ -10,29 +9,25 @@ class ContactForm(forms.Form): cleaned_data = super().clean() # Remove the full name property if "full_name" in cleaned_data: - del cleaned_data["full_name"] + full_name: str = cleaned_data["full_name"] + if full_name: + name_fields = full_name.split(" ") + + + cleaned_data["first_name"] = name_fields[0] + if len(name_fields) == 2: + cleaned_data["last_name"] = " ".join(name_fields[1:]) + elif len(name_fields) > 2: + cleaned_data["middle_name"] = name_fields[1] + cleaned_data["last_name"] = " ".join(name_fields[2:]) + else: + cleaned_data["middle_name"] = None + cleaned_data["last_name"] = None + + # Delete the full name element as we don't need it anymore + del cleaned_data["full_name"] return cleaned_data - def to_database(self, obj): - """ - Adds this form's cleaned data to `obj` and saves `obj`. - - Does nothing if form is not valid. - """ - if not self.is_valid(): - return - for name, value in self.cleaned_data.items(): - setattr(obj, name, value) - obj.save() - - @classmethod - def from_database(cls, obj): - """Returns a dict of form field values gotten from `obj`.""" - if obj is None: - return {} - return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore - - full_name = forms.CharField( label="Full name", error_messages={"required": "Enter your full name"}, @@ -57,7 +52,6 @@ class ContactForm(forms.Form): ) email = forms.EmailField( label="Organization email", - max_length=None, required=False, ) phone = PhoneNumberField( diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 69d28df52..119b78fa6 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -92,6 +92,13 @@ class Contact(TimeStampedModel): names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] return " ".join(names) if names else "Unknown" + @property + def full_name(self): + """ + Returns the full name (first_name, middle_name, last_name) of this contact. + """ + return self.get_formatted_name() + def has_contact_info(self): return bool(self.title or self.email or self.phone) @@ -126,15 +133,3 @@ class Contact(TimeStampedModel): return str(self.pk) else: return "" - - @property - def full_name(self, separator=" "): - """ - Returns the full name (first_name, middle_name, last_name) of this contact. - Seperator (which defaults to a blank space) determines the seperator for each of those fields. - For instance, with seperator=", " - this function would return this: - "First, Middle, Last" - """ - # Filter out empty strings to avoid extra spaces or separators - parts = [self.first_name or "", self.middle_name or "", self.last_name or ""] - return separator.join(parts) \ No newline at end of file diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 209c0303f..8f504ad9e 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -261,3 +261,23 @@ class CreateOrUpdateOrganizationTypeHelper: return False else: return True + + +def to_database(form, obj): + """ + Adds the form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + """ + if not form.is_valid(): + return None + for name, value in form.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + +def from_database(form_class, obj): + """Returns a dict of form field values gotten from `obj`.""" + if obj is None: + return {} + return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore \ No newline at end of file diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 783951279..2c420de96 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -23,7 +23,8 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.contact.pk}) + # redirect_to_domain_request = request.GET.get('domain_request', "") != "" + setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -32,6 +33,7 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): + # Check if 'request' query parameter is not 'True' # Redirect to the setup page return HttpResponseRedirect(setup_page) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index b9d1ca6f6..add20a5a9 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -24,7 +24,6 @@

Finish setting up your profile

- {% public_site_url 'help/account-management/#get-help-with-login.gov' %} We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public. @@ -79,9 +78,15 @@

- + {% if confirm_changes %} + + {% else %} + + {% endif %}
{% block form_fields %}{% endblock %}
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index f70f59a26..745101ade 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,9 +1,12 @@ +from django.http import HttpResponseRedirect from django.urls import reverse from registrar.forms.contact import ContactForm from registrar.models.contact import Contact +from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin - +from registrar.models.utility.generic_helper import to_database, from_database +from django.utils.safestring import mark_safe # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): @@ -30,14 +33,15 @@ class BaseContactView(ContactPermissionView): self.object = cached_contact else: self.object = self.get_object() + self._update_session_with_contact() def _update_session_with_contact(self): """ Set contact pk in the session cache """ - domain_pk = "contact:" + str(self.kwargs.get("pk")) - self.session[domain_pk] = self.object + contact_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[contact_pk] = self.object class ContactFormBaseView(BaseContactView, FormMixin): @@ -50,13 +54,9 @@ class ContactFormBaseView(BaseContactView, FormMixin): # Set the current contact object in cache self._set_contact(request) - # Get the current form and validate it form = self.get_form() - return self.check_form(form) - - # TODO rename? - def check_form(self, form): + # Get the current form and validate it return self.form_valid(form) if form.is_valid() else self.form_invalid(form) def form_invalid(self, form): @@ -74,31 +74,65 @@ class ContactProfileSetupView(ContactFormBaseView): form_class = ContactForm model = Contact + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + # Set the current contact object in cache + self._set_contact(request) + + form = self.get_form() + + # Get the current form and validate it + if form.is_valid(): + if "redirect_to_home" not in self.session or not self.session["redirect_to_home"]: + self.session["redirect_to_home"] = "contact_setup_submit_button" in request.POST + return self.form_valid(form) + else: + return self.form_invalid(form) + def get_success_url(self): """Redirect to the nameservers page for the domain.""" + # TODO - some logic should exist that navigates them to the domain request page if # they clicked it on get.gov # Add a notification that the update was successful - return reverse("home") + if "redirect_to_home" in self.session and self.session["redirect_to_home"]: + return reverse("home") + else: + # Redirect to the same page with a query parameter to confirm changes + self.session["redirect_to_home"] = True + return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) def form_valid(self, form): self.request.user.finished_setup = True self.request.user.save() - form.to_database(self.object) + to_database(form=form, obj=self.object) self._update_session_with_contact() return super().form_valid(form) def get_initial(self): """The initial value for the form (which is a formset here).""" - db_object = self.form_class.from_database(self.object) + db_object = from_database(form_class=self.form_class, obj=self.object) return db_object def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["email_sublabel_text"] = ( + context["email_sublabel_text"] = self._email_sublabel_text() + + if "redirect_to_home" in self.session and self.session["redirect_to_home"]: + context['confirm_changes'] = True + + return context + + def _email_sublabel_text(self): + """Returns the lengthy sublabel for the email field""" + help_url = public_site_url('help/account-management/#get-help-with-login.gov') + return mark_safe( "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " - "and log back in. Get help with your Login.gov account.") - return context + f'and log back in. Get help with your Login.gov account.' + ) From f1f8a6275331deefee5d7831132ad271ededdf66 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 14:34:09 -0600 Subject: [PATCH 16/66] Finish redirect logic Still needs some cleanup --- src/registrar/registrar_middleware.py | 11 ++- .../templates/finish_contact_setup.html | 10 ++- src/registrar/views/contact.py | 87 +++++++++++++++---- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 2c420de96..f357f1050 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -24,7 +24,10 @@ class CheckUserProfileMiddleware: finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: # redirect_to_domain_request = request.GET.get('domain_request', "") != "" - setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) + setup_page = reverse( + "finish-contact-profile-setup", + kwargs={"pk": request.user.contact.pk} + ) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -33,7 +36,11 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): - # Check if 'request' query parameter is not 'True' + # Preserve the original query parameters + query_params = request.GET.urlencode() + if query_params: + setup_page += f"?{query_params}" + # Redirect to the setup page return HttpResponseRedirect(setup_page) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index add20a5a9..58eee0b17 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -78,14 +78,16 @@
+ {% if confirm_changes %} - - {% else %} + {% else %} + + {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 745101ade..2c98730a7 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,3 +1,5 @@ +from enum import Enum +from urllib.parse import urlencode from django.http import HttpResponseRedirect from django.urls import reverse from registrar.forms.contact import ContactForm @@ -8,12 +10,16 @@ from django.views.generic.edit import FormMixin from registrar.models.utility.generic_helper import to_database, from_database from django.utils.safestring import mark_safe +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect + # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): def get(self, request, *args, **kwargs): self._set_contact(request) context = self.get_context_data(object=self.object) + return self.render_to_response(context) # TODO - this deserves a small refactor @@ -74,11 +80,56 @@ class ContactProfileSetupView(ContactFormBaseView): form_class = ContactForm model = Contact + redirect_type = None + class RedirectType: + HOME = "home" + BACK_TO_SELF = "back_to_self" + DOMAIN_REQUEST = "domain_request" + + @method_decorator(csrf_protect) + def dispatch(self, request, *args, **kwargs): + # Default redirect type + default_redirect = self.RedirectType.BACK_TO_SELF + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", default_redirect) + + # Store the redirect type in the session + self.redirect_type = redirect_type + + return super().dispatch(request, *args, **kwargs) + + def get_redirect_url(self): + match self.redirect_type: + case self.RedirectType.HOME: + return reverse("home") + case self.RedirectType.BACK_TO_SELF: + return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) + case self.RedirectType.DOMAIN_REQUEST: + # TODO + return reverse("home") + case _: + return reverse("home") + + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + redirect_url = self.get_redirect_url() + return redirect_url + def post(self, request, *args, **kwargs): """Form submission posts to this view. This post method harmonizes using BaseContactView and FormMixin """ + # Default redirect type + default_redirect = self.RedirectType.BACK_TO_SELF + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", default_redirect) + + # Store the redirect type in the session + self.redirect_type = redirect_type + # Set the current contact object in cache self._set_contact(request) @@ -86,28 +137,25 @@ class ContactProfileSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): - if "redirect_to_home" not in self.session or not self.session["redirect_to_home"]: - self.session["redirect_to_home"] = "contact_setup_submit_button" in request.POST + if 'contact_setup_save_button' in request.POST: + # Logic for when the 'Save' button is clicked + self.redirect_type = self.RedirectType.BACK_TO_SELF + self.session["should_redirect_to_home"] = "redirect_to_home" in request.POST + elif 'contact_setup_submit_button' in request.POST: + # Logic for when the 'Save and continue' button is clicked + if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: + self.redirect_type = self.RedirectType.HOME + else: + self.redirect_type = self.RedirectType.DOMAIN_REQUEST return self.form_valid(form) else: return self.form_invalid(form) - def get_success_url(self): - """Redirect to the nameservers page for the domain.""" - - # TODO - some logic should exist that navigates them to the domain request page if - # they clicked it on get.gov - # Add a notification that the update was successful - if "redirect_to_home" in self.session and self.session["redirect_to_home"]: - return reverse("home") - else: - # Redirect to the same page with a query parameter to confirm changes - self.session["redirect_to_home"] = True - return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - def form_valid(self, form): - self.request.user.finished_setup = True - self.request.user.save() + + if self.redirect_type == self.RedirectType.HOME: + self.request.user.finished_setup = True + self.request.user.save() to_database(form=form, obj=self.object) self._update_session_with_contact() @@ -120,11 +168,12 @@ class ContactProfileSetupView(ContactFormBaseView): return db_object def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "redirect_to_home" in self.session and self.session["redirect_to_home"]: - context['confirm_changes'] = True + if "should_redirect_to_home" in self.session: + context["confirm_changes"] = True return context From aaa9047478f369376bbdf92b3e665a2789701c9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 08:07:44 -0600 Subject: [PATCH 17/66] Add summary box to page --- src/registrar/assets/sass/_theme/_forms.scss | 3 ++ .../templates/domain_request_intro.html | 28 ++++++++++++++++++- .../templates/finish_contact_setup.html | 2 +- src/registrar/views/contact.py | 19 ++++--------- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index a4194273d..d291cf333 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -15,6 +15,9 @@ .usa-form--extra-large { max-width: none; + .usa-summary-box { + max-width: 600px; + } } .usa-form--text-width { diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 72abe6a27..28ee3a2ef 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -13,8 +13,34 @@

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.

Time to complete the form

If you have all the information you need, - completing your domain request might take around 15 minutes.

+ completing your domain request might take around 15 minutes.

+

How we’ll reach you

+

+ While reviewing your domain request, we may need to reach out with questions. + We’ll also email you when we complete our review. + If the contact information below is not correct, visit your profile to make updates. +

+ +
+
+

+ Your contact information +

+
+
    +
  • Full name: {{ user.contact.full_name }}
  • +
  • Organization email: {{ user.contact.email }}
  • +
  • Title or role in your organization: {{ user.contact.title }}
  • +
  • Phone: {{ user.contact.phone }}
  • +
+
+
+
{% block form_buttons %}
diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 58eee0b17..4c9a6595b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -87,7 +87,7 @@ - + {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 2c98730a7..b7396f734 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -94,7 +94,6 @@ class ContactProfileSetupView(ContactFormBaseView): # Update redirect type based on the query parameter if present redirect_type = request.GET.get("redirect", default_redirect) - # Store the redirect type in the session self.redirect_type = redirect_type return super().dispatch(request, *args, **kwargs) @@ -106,8 +105,7 @@ class ContactProfileSetupView(ContactFormBaseView): case self.RedirectType.BACK_TO_SELF: return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) case self.RedirectType.DOMAIN_REQUEST: - # TODO - return reverse("home") + return reverse("domain-request:") case _: return reverse("home") @@ -116,19 +114,12 @@ class ContactProfileSetupView(ContactFormBaseView): redirect_url = self.get_redirect_url() return redirect_url + # TODO - delete session information def post(self, request, *args, **kwargs): """Form submission posts to this view. This post method harmonizes using BaseContactView and FormMixin """ - # Default redirect type - default_redirect = self.RedirectType.BACK_TO_SELF - - # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", default_redirect) - - # Store the redirect type in the session - self.redirect_type = redirect_type # Set the current contact object in cache self._set_contact(request) @@ -140,7 +131,7 @@ class ContactProfileSetupView(ContactFormBaseView): if 'contact_setup_save_button' in request.POST: # Logic for when the 'Save' button is clicked self.redirect_type = self.RedirectType.BACK_TO_SELF - self.session["should_redirect_to_home"] = "redirect_to_home" in request.POST + self.session["should_redirect"] = "redirect_to_confirmation_page" in request.POST elif 'contact_setup_submit_button' in request.POST: # Logic for when the 'Save and continue' button is clicked if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: @@ -153,7 +144,7 @@ class ContactProfileSetupView(ContactFormBaseView): def form_valid(self, form): - if self.redirect_type == self.RedirectType.HOME: + if self.redirect_type != self.RedirectType.BACK_TO_SELF: self.request.user.finished_setup = True self.request.user.save() @@ -172,7 +163,7 @@ class ContactProfileSetupView(ContactFormBaseView): context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "should_redirect_to_home" in self.session: + if "should_redirect" in self.session: context["confirm_changes"] = True return context From b741540bdfd18ec495b33a6975daf87a3eb99775 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 10:03:10 -0600 Subject: [PATCH 18/66] Polishing touches --- src/registrar/assets/js/get-gov.js | 53 ++++++++++++------- src/registrar/assets/sass/_theme/_forms.scss | 11 ++++ src/registrar/forms/contact.py | 22 ++------ .../templates/finish_contact_setup.html | 10 ++-- .../templates/includes/readonly_input.html | 4 +- .../templates/includes/required_fields.html | 2 +- 6 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 73afd8131..98a08fc2c 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -842,6 +842,14 @@ function hideDeletedForms() { */ (function finishUserSetupListener() { + function getInputFieldId(fieldName){ + return `#id_${fieldName}` + } + + function getReadonlyFieldId(fieldName){ + return `#${fieldName}__edit-button-readonly` + } + // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { let inputId = getInputFieldId(fieldName) @@ -854,29 +862,19 @@ function hideDeletedForms() { inputField.classList.toggle('display-none'); // Toggle the bold style on the grid row - let formGroup = button.closest(".usa-form-group") - if (formGroup){ - gridRow = button.querySelector(".grid-row") - if (gridRow){ - gridRow.toggle("bold-usa-label") - } + let gridRow = button.closest(".grid-col-2").closest(".grid-row") + if (gridRow){ + gridRow.classList.toggle("bold-usa-label") } } - function getInputFieldId(fieldName){ - return `#id_${fieldName}` - } - - function getReadonlyFieldId(fieldName){ - return `#${fieldName}__edit-button-readonly` - } - function handleFullNameField(fieldName, nameFields) { // Remove the display-none class from the nearest parent div let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); if (inputField) { + // Show each name field nameFields.forEach(function(fieldName) { let nameId = getInputFieldId(fieldName) let nameField = document.querySelector(nameId); @@ -887,7 +885,14 @@ function hideDeletedForms() { } } }); + + // Remove the "full_name" field + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.remove(); + } } + } function handleEditButtonClick(fieldName, button){ @@ -895,8 +900,14 @@ function hideDeletedForms() { // Lock the edit button while this operation occurs button.disabled = true - showInputFieldHideReadonlyField(fieldName, button); - button.classList.add('display-none'); + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + showInputFieldHideReadonlyField(fieldName, button); + } + + button.classList.add("display-none"); // Unlock after it completes button.disabled = false @@ -925,10 +936,16 @@ function hideDeletedForms() { let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { + let nameFields = ["first_name", "middle_name", "last_name"] + // If either the full_name field errors out, + // or if any of its associated fields do - show all name related fields. + // Otherwise, just show the problematic field. if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] handleFullNameField(fieldName, nameFields); - }else { + }else if (nameFields.includes(fieldName)){ + handleFullNameField("full_name", nameFields); + } + else { button.click() } } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index d291cf333..b198bbc18 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -38,6 +38,10 @@ font-weight: bold; } + &.bold-usa-label label.usa-label{ + font-weight: bold; + } + } .usa-form-readonly:first-of-type { @@ -62,6 +66,13 @@ legend.float-left-tablet + button.float-right-tablet { } } +@media (min-width: 35em) { + .usa-form--largest { + max-width: 35rem; + } +} + + // Custom style for disabled inputs @media (prefers-color-scheme: light) { .usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled { diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 83b49f548..fa0bf1fce 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -9,23 +9,10 @@ class ContactForm(forms.Form): cleaned_data = super().clean() # Remove the full name property if "full_name" in cleaned_data: - full_name: str = cleaned_data["full_name"] - if full_name: - name_fields = full_name.split(" ") - - - cleaned_data["first_name"] = name_fields[0] - if len(name_fields) == 2: - cleaned_data["last_name"] = " ".join(name_fields[1:]) - elif len(name_fields) > 2: - cleaned_data["middle_name"] = name_fields[1] - cleaned_data["last_name"] = " ".join(name_fields[2:]) - else: - cleaned_data["middle_name"] = None - cleaned_data["last_name"] = None - - # Delete the full name element as we don't need it anymore - del cleaned_data["full_name"] + # Delete the full name element as its purely decorative. + # We include it as a normal Charfield for all the advantages + # and utility that it brings, but we're playing pretend. + del cleaned_data["full_name"] return cleaned_data full_name = forms.CharField( @@ -53,6 +40,7 @@ class ContactForm(forms.Form): email = forms.EmailField( label="Organization email", required=False, + max_length=None, ) phone = PhoneNumberField( label="Phone", diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 4c9a6595b..e856927bb 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -34,9 +34,9 @@ Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

- {# TODO: maybe remove this? #} -

Required information is marked with an asterisk (*).

- + {# We use a var called 'remove_margin_top' rather than 'add_margin_top' because most fields use this #} + {% include "includes/required_fields.html" with remove_margin_top=True %} + {% csrf_token %}
@@ -61,8 +61,8 @@ {% input_with_errors form.last_name %} {% endwith %} - {# TODO: I shouldnt need to do add_class here #} - {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2" sublabel_text=email_sublabel_text %} + {# This field doesn't have the readonly button but it has common design elements from it #} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2 bold-usa-label" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} {% endwith %} diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html index 820d4b66c..59a55090c 100644 --- a/src/registrar/templates/includes/readonly_input.html +++ b/src/registrar/templates/includes/readonly_input.html @@ -1,6 +1,6 @@ {% load static field_helpers url_helpers %} -
+
-
+
{{ field.value }}
diff --git a/src/registrar/templates/includes/required_fields.html b/src/registrar/templates/includes/required_fields.html index 0087b048a..be0395979 100644 --- a/src/registrar/templates/includes/required_fields.html +++ b/src/registrar/templates/includes/required_fields.html @@ -1,3 +1,3 @@ -

+

Required fields are marked with an asterisk (*).

From 1fd58d6d843b916dd7551b6c636b887867c10342 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 11:49:55 -0600 Subject: [PATCH 19/66] Redirect logic --- .../templates/finish_contact_setup.html | 1 - src/registrar/views/contact.py | 129 ++++++++++++++---- 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index e856927bb..68e860267 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -87,7 +87,6 @@ - {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index b7396f734..0fbd92435 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,7 +1,6 @@ from enum import Enum -from urllib.parse import urlencode -from django.http import HttpResponseRedirect -from django.urls import reverse +from urllib.parse import urlencode, urlunparse, urlparse, quote +from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url @@ -13,19 +12,23 @@ from django.utils.safestring import mark_safe from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_protect -# TODO we can and probably should generalize this at this rate. +import logging + +logger = logging.getLogger(__name__) + + class BaseContactView(ContactPermissionView): def get(self, request, *args, **kwargs): + """Sets the current contact in cache, defines the current object as self.object + then returns render_to_response""" self._set_contact(request) context = self.get_context_data(object=self.object) - return self.render_to_response(context) - # TODO - this deserves a small refactor def _set_contact(self, request): """ - get domain from session cache or from db and set + get contact from session cache or from db and set to self.object set session to self for downstream functions to update session cache @@ -81,34 +84,99 @@ class ContactProfileSetupView(ContactFormBaseView): model = Contact redirect_type = None + + # TODO - make this an enum class RedirectType: + """ + Contains constants for each type of redirection. + Not an enum as we just need to track string values, + but we don't care about enforcing it. + + - HOME: We want to redirect to reverse("home") + - BACK_TO_SELF: We want to redirect back to reverse("finish-contact-profile-setup") + - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" + - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent + redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE + """ HOME = "home" BACK_TO_SELF = "back_to_self" - DOMAIN_REQUEST = "domain_request" + COMPLETE_SETUP = "complete_setup" + TO_SPECIFIC_PAGE = "domain_request" + # TODO - refactor @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): + # Default redirect type default_redirect = self.RedirectType.BACK_TO_SELF # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", default_redirect) + redirect_type = request.GET.get("redirect", None) - self.redirect_type = redirect_type + is_default = False + # We set this here rather than in .get so we don't override + # existing data if no queryparam is present. + if redirect_type is None: + is_default = True + redirect_type = default_redirect + + # Set the default if nothing exists already + if self.redirect_type is None: + self.redirect_type = redirect_type + + if not is_default: + default_redirects = [ + self.RedirectType.HOME, + self.RedirectType.COMPLETE_SETUP, + self.RedirectType.BACK_TO_SELF, + self.RedirectType.TO_SPECIFIC_PAGE + ] + if redirect_type not in default_redirects: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + request.session["profile_setup_redirect_viewname"] = redirect_type + else: + self.redirect_type = redirect_type return super().dispatch(request, *args, **kwargs) def get_redirect_url(self): + base_url = "" + query_params = {} match self.redirect_type: case self.RedirectType.HOME: - return reverse("home") - case self.RedirectType.BACK_TO_SELF: - return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - case self.RedirectType.DOMAIN_REQUEST: - return reverse("domain-request:") + base_url = reverse("home") + case self.RedirectType.BACK_TO_SELF | self.RedirectType.COMPLETE_SETUP: + base_url = reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) + case self.RedirectType.TO_SPECIFIC_PAGE: + + # We only allow this session value to use viewnames, + # because otherwise this allows anyone to enter any value in here. + # This restricts what can be redirected to. + try: + desired_view = self.session["profile_setup_redirect_viewname"] + base_url = reverse(desired_view) + except NoReverseMatch as err: + logger.error(err) + logger.error( + "ContactProfileSetupView -> get_redirect_url -> Could not find specified page." + ) + base_url = reverse("home") case _: - return reverse("home") - + base_url = reverse("home") + + # Quote cleans up the value so that it can be used in a url + query_params["redirect"] = quote(self.redirect_type) + + # Parse the base URL + url_parts = list(urlparse(base_url)) + + # Update the query part of the URL + url_parts[4] = urlencode(query_params) + + # Construct the full URL with query parameters + full_url = urlunparse(url_parts) + return full_url + def get_success_url(self): """Redirect to the nameservers page for the domain.""" redirect_url = self.get_redirect_url() @@ -128,23 +196,26 @@ class ContactProfileSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): - if 'contact_setup_save_button' in request.POST: + if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked - self.redirect_type = self.RedirectType.BACK_TO_SELF - self.session["should_redirect"] = "redirect_to_confirmation_page" in request.POST - elif 'contact_setup_submit_button' in request.POST: - # Logic for when the 'Save and continue' button is clicked - if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: - self.redirect_type = self.RedirectType.HOME + self.redirect_type = self.RedirectType.COMPLETE_SETUP + elif "contact_setup_submit_button" in request.POST: + if "profile_setup_redirect_viewname" in self.session: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE else: - self.redirect_type = self.RedirectType.DOMAIN_REQUEST + self.redirect_type = self.RedirectType.HOME + return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): - - if self.redirect_type != self.RedirectType.BACK_TO_SELF: + + completed_states = [ + self.RedirectType.TO_SPECIFIC_PAGE, + self.RedirectType.HOME + ] + if self.redirect_type in completed_states: self.request.user.finished_setup = True self.request.user.save() @@ -163,14 +234,14 @@ class ContactProfileSetupView(ContactFormBaseView): context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "should_redirect" in self.session: + if self.redirect_type == self.RedirectType.COMPLETE_SETUP: context["confirm_changes"] = True return context def _email_sublabel_text(self): """Returns the lengthy sublabel for the email field""" - help_url = public_site_url('help/account-management/#get-help-with-login.gov') + help_url = public_site_url("help/account-management/#get-help-with-login.gov") return mark_safe( "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " From c352869f6be066c3b3718227a047b073c958ae0e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 12:40:29 -0600 Subject: [PATCH 20/66] Redirect logic from home --- src/registrar/registrar_middleware.py | 30 ++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index f357f1050..a9b2f7a23 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -1,8 +1,8 @@ """ Contains middleware used in settings.py """ - -from django.urls import reverse +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from django.urls import reverse, resolve from django.http import HttpResponseRedirect class CheckUserProfileMiddleware: @@ -20,6 +20,8 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): + + # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: @@ -33,13 +35,31 @@ class CheckUserProfileMiddleware: setup_page, logout_page, ] + custom_redirect = None + + # In some cases, we don't want to redirect to home. + # This handles that. + if request.path == "/request/": + # This can be generalized if need be, but for now lets keep this easy to read. + custom_redirect = "domain-request:" # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): - # Preserve the original query parameters - query_params = request.GET.urlencode() + # Preserve the original query parameters, and coerce them into a dict + query_params = parse_qs(request.META['QUERY_STRING']) + + if custom_redirect is not None: + # Set the redirect value to our redirect location + query_params["redirect"] = custom_redirect + if query_params: - setup_page += f"?{query_params}" + # Split the URL into parts + setup_page_parts = list(urlparse(setup_page)) + # Modify the query param bit + setup_page_parts[4] = urlencode(query_params) + # Reassemble the URL + setup_page = urlunparse(setup_page_parts) + # Redirect to the setup page return HttpResponseRedirect(setup_page) From 059a99aca1bd62812c6d549b9caab607c17e75fb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:26:48 -0600 Subject: [PATCH 21/66] Add waffle flags --- src/registrar/registrar_middleware.py | 10 ++++++++-- src/registrar/views/contact.py | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index a9b2f7a23..427775f34 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -2,8 +2,9 @@ Contains middleware used in settings.py """ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode -from django.urls import reverse, resolve +from django.urls import reverse from django.http import HttpResponseRedirect +from waffle.decorators import flag_is_active class CheckUserProfileMiddleware: """ @@ -20,12 +21,17 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): + + # Check that the user is "opted-in" to the profile feature flag + has_profile_feature_flag = flag_is_active(request, "profile_feature") + # If they aren't, skip this entirely + if not has_profile_feature_flag: + return None # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - # redirect_to_domain_request = request.GET.get('domain_request', "") != "" setup_page = reverse( "finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 0fbd92435..f2f5e29a9 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,4 +1,4 @@ -from enum import Enum +from waffle.decorators import waffle_flag from urllib.parse import urlencode, urlunparse, urlparse, quote from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) class BaseContactView(ContactPermissionView): - + """Provides a base view for the contact object. On get, the contact + is saved in the session and on self.object.""" def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -54,7 +55,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): - + """Adds a FormMixin to BaseContactView, and handles post""" def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -104,6 +105,7 @@ class ContactProfileSetupView(ContactFormBaseView): TO_SPECIFIC_PAGE = "domain_request" # TODO - refactor + @waffle_flag('profile_feature') @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): @@ -140,6 +142,16 @@ class ContactProfileSetupView(ContactFormBaseView): return super().dispatch(request, *args, **kwargs) def get_redirect_url(self): + """ + Returns a URL string based on the current value of self.redirect_type. + + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as + HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. + + Returns: + str: The full URL with the appropriate query parameters. + """ base_url = "" query_params = {} match self.redirect_type: From 54c532f3b241df5f64b7f5857ee8314ef3e1fdf2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:33:07 -0600 Subject: [PATCH 22/66] Add comment on dispatch --- src/registrar/views/contact.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index f2f5e29a9..d8ea5a041 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -108,7 +108,16 @@ class ContactProfileSetupView(ContactFormBaseView): @waffle_flag('profile_feature') @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): - + """ + Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. + + This method sets the redirect type based on the 'redirect' query parameter, + defaulting to BACK_TO_SELF if not provided. + It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + + Returns: + HttpResponse: The response generated by the parent class's dispatch method. + """ # Default redirect type default_redirect = self.RedirectType.BACK_TO_SELF From f55d0a655a7e4093d8ffbe27b87fa096fa6d2693 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:49:43 -0600 Subject: [PATCH 23/66] Cleanup --- src/djangooidc/backends.py | 5 ++- src/registrar/forms/contact.py | 1 - src/registrar/forms/domain.py | 1 + src/registrar/forms/domain_request_wizard.py | 2 +- src/registrar/models/contact.py | 1 + .../models/utility/generic_helper.py | 2 +- src/registrar/registrar_middleware.py | 17 ++++---- src/registrar/views/contact.py | 43 +++++++++---------- src/registrar/views/domain_request.py | 1 - src/registrar/views/utility/mixins.py | 8 +--- 10 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 96b7a902a..8e4eb023d 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -50,7 +50,7 @@ class OpenIdConnectBackend(ModelBackend): user, created = UserModel.objects.get_or_create(**args) - if created: + if created and request is not None: request.session["is_new_user"] = True if not created: @@ -63,7 +63,8 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - request.session["is_new_user"] = True + if request is not None: + request.session["is_new_user"] = True return None # run this callback for a each login user.on_each_login() diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index fa0bf1fce..9a752bab4 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -46,4 +46,3 @@ class ContactForm(forms.Form): label="Phone", error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, ) - diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 9dfd9773a..db247ad21 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -202,6 +202,7 @@ NameserverFormset = formset_factory( validate_max=True, ) + # TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 9d16a30de..dd29df523 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -150,7 +150,7 @@ class OrganizationContactForm(RegistrarForm): """Require something to be selected when this is a federal agency.""" federal_agency = self.cleaned_data.get("federal_agency", None) # need the domain request object to know if this is federal - if self.domain_request is None: + if hasattr(self, "domain_request") and self.domain_request is None: # hmm, no saved domain request object?, default require the agency if not federal_agency: # no answer was selected diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 119b78fa6..6e196783b 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -3,6 +3,7 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel from phonenumber_field.modelfields import PhoneNumberField # type: ignore + class Contact(TimeStampedModel): """Contact information follows a similar pattern for each contact.""" diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 8f504ad9e..6ee598c13 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -280,4 +280,4 @@ def from_database(form_class, obj): """Returns a dict of form field values gotten from `obj`.""" if obj is None: return {} - return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore \ No newline at end of file + return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 427775f34..8dca06019 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -1,17 +1,20 @@ """ Contains middleware used in settings.py """ + from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from django.urls import reverse from django.http import HttpResponseRedirect from waffle.decorators import flag_is_active + class CheckUserProfileMiddleware: """ - Checks if the current user has finished_setup = False. + Checks if the current user has finished_setup = False. If they do, redirect them to the setup page regardless of where they are in the application. """ + def __init__(self, get_response): self.get_response = get_response @@ -21,7 +24,7 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): - + # Check that the user is "opted-in" to the profile feature flag has_profile_feature_flag = flag_is_active(request, "profile_feature") @@ -32,10 +35,7 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse( - "finish-contact-profile-setup", - kwargs={"pk": request.user.contact.pk} - ) + setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -52,7 +52,7 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): # 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"]) if custom_redirect is not None: # Set the redirect value to our redirect location @@ -65,7 +65,6 @@ class CheckUserProfileMiddleware: setup_page_parts[4] = urlencode(query_params) # Reassemble the URL setup_page = urlunparse(setup_page_parts) - # Redirect to the setup page return HttpResponseRedirect(setup_page) @@ -88,4 +87,4 @@ class NoCacheMiddleware: def __call__(self, request): response = self.get_response(request) response["Cache-Control"] = "no-cache" - return response \ No newline at end of file + return response diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index d8ea5a041..322630b07 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) class BaseContactView(ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" + def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -56,6 +57,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): """Adds a FormMixin to BaseContactView, and handles post""" + def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -78,8 +80,9 @@ class ContactFormBaseView(BaseContactView, FormMixin): class ContactProfileSetupView(ContactFormBaseView): - """This view forces the user into providing additional details that + """This view forces the user into providing additional details that we may have missed from Login.gov""" + template_name = "finish_contact_setup.html" form_class = ContactForm model = Contact @@ -99,22 +102,23 @@ class ContactProfileSetupView(ContactFormBaseView): - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE """ + HOME = "home" BACK_TO_SELF = "back_to_self" COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" # TODO - refactor - @waffle_flag('profile_feature') + @waffle_flag("profile_feature") @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): """ Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - - This method sets the redirect type based on the 'redirect' query parameter, + + This method sets the redirect type based on the 'redirect' query parameter, defaulting to BACK_TO_SELF if not provided. It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. - + Returns: HttpResponse: The response generated by the parent class's dispatch method. """ @@ -140,7 +144,7 @@ class ContactProfileSetupView(ContactFormBaseView): self.RedirectType.HOME, self.RedirectType.COMPLETE_SETUP, self.RedirectType.BACK_TO_SELF, - self.RedirectType.TO_SPECIFIC_PAGE + self.RedirectType.TO_SPECIFIC_PAGE, ] if redirect_type not in default_redirects: self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE @@ -154,8 +158,8 @@ class ContactProfileSetupView(ContactFormBaseView): """ Returns a URL string based on the current value of self.redirect_type. - Depending on self.redirect_type, constructs a base URL and appends a - 'redirect' query parameter. Handles different redirection types such as + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. Returns: @@ -178,13 +182,11 @@ class ContactProfileSetupView(ContactFormBaseView): base_url = reverse(desired_view) except NoReverseMatch as err: logger.error(err) - logger.error( - "ContactProfileSetupView -> get_redirect_url -> Could not find specified page." - ) + logger.error("ContactProfileSetupView -> get_redirect_url -> Could not find specified page.") base_url = reverse("home") case _: base_url = reverse("home") - + # Quote cleans up the value so that it can be used in a url query_params["redirect"] = quote(self.redirect_type) @@ -231,27 +233,24 @@ class ContactProfileSetupView(ContactFormBaseView): return self.form_invalid(form) def form_valid(self, form): - - completed_states = [ - self.RedirectType.TO_SPECIFIC_PAGE, - self.RedirectType.HOME - ] + + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] if self.redirect_type in completed_states: self.request.user.finished_setup = True self.request.user.save() - + to_database(form=form, obj=self.object) self._update_session_with_contact() return super().form_valid(form) - + def get_initial(self): """The initial value for the form (which is a formset here).""" db_object = from_database(form_class=self.form_class, obj=self.object) return db_object - + def get_context_data(self, **kwargs): - + context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() @@ -259,7 +258,7 @@ class ContactProfileSetupView(ContactFormBaseView): context["confirm_changes"] = True return context - + def _email_sublabel_text(self): """Returns the lengthy sublabel for the email field""" help_url = public_site_url("help/account-management/#get-help-with-login.gov") diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index b07f0d53f..f93976138 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -819,4 +819,3 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates - diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 49d172971..3e5e10816 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -341,7 +341,6 @@ class ContactPermission(PermissionsLoginMixin): if not self.request.user.is_authenticated: return False - given_contact_pk = self.kwargs["pk"] # Grab the user in the DB to do a full object comparision, not just on ids @@ -351,13 +350,10 @@ class ContactPermission(PermissionsLoginMixin): if current_user.contact.pk != given_contact_pk: # Don't allow users to modify other users profiles return False - + # Check if the object at the id we're searching on actually exists requested_user_exists = User.objects.filter(pk=current_user.pk).exists() - requested_contact_exists = Contact.objects.filter( - user=current_user.pk, - pk=given_contact_pk - ).exists() + requested_contact_exists = Contact.objects.filter(user=current_user.pk, pk=given_contact_pk).exists() if not requested_user_exists or not requested_contact_exists: return False From 56e21f4c8ef33421fbec68ad3effe57e9ba02239 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 14:30:48 -0600 Subject: [PATCH 24/66] Remove old code --- src/registrar/forms/domain_request_wizard.py | 2 +- src/registrar/forms/utility/wizard_form_helper.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index dd29df523..9d16a30de 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -150,7 +150,7 @@ class OrganizationContactForm(RegistrarForm): """Require something to be selected when this is a federal agency.""" federal_agency = self.cleaned_data.get("federal_agency", None) # need the domain request object to know if this is federal - if hasattr(self, "domain_request") and self.domain_request is None: + if self.domain_request is None: # hmm, no saved domain request object?, default require the agency if not federal_agency: # no answer was selected diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 9b8a7c4d8..2dd1a2b42 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -21,8 +21,7 @@ class RegistrarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object - if "domain_request" in kwargs: - self.domain_request = kwargs.pop("domain_request", None) + self.domain_request = kwargs.pop("domain_request", None) super(RegistrarForm, self).__init__(*args, **kwargs) From 21f5c43b34c28fbba80a3895ae4e058e186bb284 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 15:12:39 -0600 Subject: [PATCH 25/66] Add success message --- src/djangooidc/views.py | 46 ++++++++++++++++++++-------------- src/registrar/views/contact.py | 9 ++++++- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 7b5c58527..bf5fe379e 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -101,25 +101,9 @@ def login_callback(request): user = authenticate(request=request, **userinfo) is_new_user = request.session.get("is_new_user", False) if user: - should_update_user = False - # Fixture users kind of exist in a superposition of verification types, - # because while the system "verified" them, if they login, - # we don't know how the user themselves was verified through login.gov until - # they actually try logging in. This edge-case only matters in non-production environments. - fixture_user = User.VerificationTypeChoices.FIXTURE_USER - is_fixture_user = user.verification_type and user.verification_type == fixture_user - - # Set the verification type if it doesn't already exist or if its a fixture user - if not user.verification_type or is_fixture_user: - user.set_user_verification_type() - should_update_user = True - - if is_new_user: - user.finished_setup = False - should_update_user = True - - if should_update_user: - user.save() + # Set login metadata about this user + # (verification_type for instance) + _set_authenticated_user_metadata(user, is_new_user) login(request, user) @@ -149,6 +133,30 @@ def login_callback(request): return error_page(request, err) +def _set_authenticated_user_metadata(user, is_new_user): + """Does checks on the recieved authenticated user from login_callback, + and updates fields accordingly. U""" + should_update_user = False + # Fixture users kind of exist in a superposition of verification types, + # because while the system "verified" them, if they login, + # we don't know how the user themselves was verified through login.gov until + # they actually try logging in. This edge-case only matters in non-production environments. + fixture_user = User.VerificationTypeChoices.FIXTURE_USER + is_fixture_user = user.verification_type and user.verification_type == fixture_user + + # Set the verification type if it doesn't already exist or if its a fixture user + if not user.verification_type or is_fixture_user: + user.set_user_verification_type() + should_update_user = True + + if is_new_user: + user.finished_setup = False + should_update_user = True + + if should_update_user: + user.save() + + def _requires_step_up_auth(userinfo): """if User.needs_identity_verification and step_up_acr_value not in ial returned from callback, return True""" diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 322630b07..a0b99c4a0 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -2,6 +2,7 @@ from waffle.decorators import waffle_flag from urllib.parse import urlencode, urlunparse, urlparse, quote from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm +from django.contrib.messages.views import SuccessMessageMixin from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView @@ -17,10 +18,13 @@ import logging logger = logging.getLogger(__name__) -class BaseContactView(ContactPermissionView): +class BaseContactView(SuccessMessageMixin, ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" + def get_success_message(self): + return "Contact updated successfully." + def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -108,6 +112,9 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" + def get_success_message(self): + return "Your profile has been successfully updated." + # TODO - refactor @waffle_flag("profile_feature") @method_decorator(csrf_protect) From f06620cef468d07bf200cef063c5c3ede04ae0bb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 15:18:10 -0600 Subject: [PATCH 26/66] Add nosec on mark_safe We directly control help_url, so this is not a problem --- src/registrar/views/contact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index a0b99c4a0..de3525efb 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -273,4 +273,4 @@ class ContactProfileSetupView(ContactFormBaseView): "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " f'and log back in. Get help with your Login.gov account.' - ) + ) # nosec From 6c5e05f89b7924323b6c56bb7021599b086fcbae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 11:13:03 -0600 Subject: [PATCH 27/66] Tooltips, style refinement --- src/registrar/assets/js/get-gov.js | 51 ++++++++++------- src/registrar/assets/sass/_theme/_base.scss | 10 ++++ .../assets/sass/_theme/_cisa_colors.scss | 1 + .../assets/sass/_theme/_fieldsets.scss | 16 ++++++ .../assets/sass/_theme/_headers.scss | 14 +++++ .../assets/sass/_theme/_tooltips.scss | 7 +++ src/registrar/assets/sass/_theme/styles.scss | 1 + src/registrar/templates/base.html | 10 ++-- .../templates/finish_contact_setup.html | 55 ++++++++++++++----- src/registrar/templates/includes/footer.html | 2 + .../templates/includes/gov_extended_logo.html | 17 ++++++ .../includes/label_with_edit_button.html | 3 +- src/registrar/views/contact.py | 6 +- 13 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_headers.scss create mode 100644 src/registrar/templates/includes/gov_extended_logo.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 98a08fc2c..d18129456 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -868,31 +868,42 @@ function hideDeletedForms() { } } - function handleFullNameField(fieldName, nameFields) { + function handleFullNameField(fieldName) { // Remove the display-none class from the nearest parent div let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); + let nameFieldset = document.querySelector("#contact-full-name-fieldset"); + if (nameFieldset){ + nameFieldset.classList.remove("display-none"); + } + if (inputField) { - // Show each name field - nameFields.forEach(function(fieldName) { - let nameId = getInputFieldId(fieldName) - let nameField = document.querySelector(nameId); - if (nameField){ - let parentDiv = nameField.closest("div"); - if (parentDiv) { - parentDiv.classList.remove("display-none"); + let readonlyId = getReadonlyFieldId(fieldName) + let readonlyField = document.querySelector(readonlyId) + if (readonlyField) { + // Update the element's xlink:href attribute + let useElement = readonlyField.querySelector("use"); + if (useElement) { + let currentHref = useElement.getAttribute("xlink:href"); + let parts = currentHref.split("#"); + + // Update the icon reference to the info icon + if (parts.length > 1) { + parts[1] = "info"; + useElement.setAttribute("xlink:href", parts.join("#")); + + // Change the color to => $dhs-dark-gray-60 + useElement.closest('svg').style.fill = '#444547'; } } - }); - - // Remove the "full_name" field - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.remove(); + + let parentDiv = readonlyField.closest("div"); + if (parentDiv) { + parentDiv.classList.toggle("overlapped-full-name-field"); + } } } - } function handleEditButtonClick(fieldName, button){ @@ -901,8 +912,7 @@ function hideDeletedForms() { button.disabled = true if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); + handleFullNameField(fieldName); }else { showInputFieldHideReadonlyField(fieldName, button); } @@ -941,9 +951,10 @@ function hideDeletedForms() { // or if any of its associated fields do - show all name related fields. // Otherwise, just show the problematic field. if (fieldName == "full_name"){ - handleFullNameField(fieldName, nameFields); + handleFullNameField(fieldName); }else if (nameFields.includes(fieldName)){ - handleFullNameField("full_name", nameFields); + handleFullNameField("full_name"); + button.click() } else { button.click() diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index beb63cdd1..2cdaefb7d 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -170,3 +170,13 @@ abbr[title] { height: 1.25em !important; } } + +// todo - move this to a better location + better name +@media (min-width: 800px){ + .overlapped-full-name-field { + position: absolute; + top: 325px; + background-color: white; + padding-right: 8px; + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_cisa_colors.scss b/src/registrar/assets/sass/_theme/_cisa_colors.scss index 7466a3490..23ecf7989 100644 --- a/src/registrar/assets/sass/_theme/_cisa_colors.scss +++ b/src/registrar/assets/sass/_theme/_cisa_colors.scss @@ -46,6 +46,7 @@ $dhs-gray-10: #fcfdfd; /*--- Dark Gray ---*/ $dhs-dark-gray-90: #040404; +$dhs-dark-gray-85: #1b1b1b; $dhs-dark-gray-80: #19191a; $dhs-dark-gray-70: #2f2f30; $dhs-dark-gray-60: #444547; diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index c60080cb9..5cdd52026 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -8,3 +8,19 @@ fieldset { fieldset:not(:first-child) { margin-top: units(2); } + +fieldset.registrar-fieldset__contact { + border-width: 2px; + border-left: none; + border-right: none; + border-bottom: none; + padding-bottom: 0; +} + +@media (max-width: 800px){ + fieldset.registrar-fieldset__contact { + padding: 0; + margin: 0; + border: none; + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_headers.scss b/src/registrar/assets/sass/_theme/_headers.scss new file mode 100644 index 000000000..36868fdd3 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_headers.scss @@ -0,0 +1,14 @@ +@use "uswds-core" as *; +@use "cisa_colors" as *; + +.usa-logo button { + color: #{$dhs-dark-gray-85}; + font-weight: 700; + font-family: family('sans'); + font-size: 1.6rem; + line-height: 1.1; +} + +.usa-logo button.usa-button--unstyled.disabled-button:hover{ + color: #{$dhs-dark-gray-85}; +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/sass/_theme/_tooltips.scss index 04c6f3cda..203bb4f85 100644 --- a/src/registrar/assets/sass/_theme/_tooltips.scss +++ b/src/registrar/assets/sass/_theme/_tooltips.scss @@ -24,3 +24,10 @@ text-align: center !important; } } + +.usa-tooltip--registrar-logo .usa-tooltip__body { + max-width: 50px !important; + font-weight: 400 !important; + white-space: normal; + text-align: center; +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 64b113a29..e24618a23 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -20,6 +20,7 @@ @forward "tables"; @forward "sidenav"; @forward "register-form"; +@forward "_headers"; /*-------------------------------------------------- --- Admin ---------------------------------*/ diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index c0702e78f..3b541d946 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -138,11 +138,7 @@
{% block logo %} - + {% include "includes/gov_extended_logo.html" with logo_clickable=True %} {% endblock %}
@@ -199,7 +195,9 @@
{% endblock wrapper%} - {% include "includes/footer.html" %} + {% block footer %} + {% include "includes/footer.html" with show_manage_your_domains=True %} + {% endblock footer %}
{% block init_js %}{% endblock %}{# useful for vars and other initializations #} diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 68e860267..301f5837a 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -1,16 +1,36 @@ {% extends "base.html" %} {% load static form_helpers url_helpers field_helpers %} -{% block title %} Finish setting up your profile {% endblock %} +{% block title %} Finish setting up your profile | {% endblock %} + +{# Disable the redirect #} +{% block logo %} + {% include "includes/gov_extended_logo.html" with logo_clickable=False %} +{% endblock %} {% block content %}
- {% include "includes/form_errors.html" with form=form %} + {% comment %} - Repurposed from domain_request_form.html + Form success messages. {% endcomment %} + {% if messages %} + {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %} + {% endif %} + + {% comment %} + Repurposed from domain_request_form.html. + Form errors. + {% endcomment %} + {% include "includes/form_errors.html" with form=form %} {% for outer in forms %} {% if outer|isformset %} {% for inner in outer.forms %} @@ -48,19 +68,21 @@ {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} - {% input_with_errors form.first_name %} - {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} - {% input_with_errors form.middle_name %} - {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none"%} - {% input_with_errors form.last_name %} - {% endwith %} + + {# This field doesn't have the readonly button but it has common design elements from it #} {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2 bold-usa-label" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} @@ -97,3 +119,6 @@ {% endblock content %} +{% block footer %} + {% include "includes/footer.html" %} +{% endblock footer %} \ No newline at end of file diff --git a/src/registrar/templates/includes/footer.html b/src/registrar/templates/includes/footer.html index 303c0ef74..5e10955e4 100644 --- a/src/registrar/templates/includes/footer.html +++ b/src/registrar/templates/includes/footer.html @@ -26,10 +26,12 @@ >
\ No newline at end of file diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index de3525efb..257f0db01 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -22,7 +22,8 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" - def get_success_message(self): + def get_success_message(self, cleaned_data): + """Content of the returned success message""" return "Contact updated successfully." def get(self, request, *args, **kwargs): @@ -112,7 +113,8 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" - def get_success_message(self): + def get_success_message(self, cleaned_data): + """Content of the returned success message""" return "Your profile has been successfully updated." # TODO - refactor From b2e52b52676ff7d56450d81625cb8bb4998a7c20 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 11:28:56 -0600 Subject: [PATCH 28/66] Pixel adjustment --- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/assets/sass/_theme/_fieldsets.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 2cdaefb7d..3fe5c6f68 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -175,7 +175,7 @@ abbr[title] { @media (min-width: 800px){ .overlapped-full-name-field { position: absolute; - top: 325px; + top: 337px; background-color: white; padding-right: 8px; } diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index 5cdd52026..da900f128 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -17,6 +17,12 @@ fieldset.registrar-fieldset__contact { padding-bottom: 0; } +@media (min-width: 800px) { + fieldset.registrar-fieldset__contact { + margin-top: 28px; + } +} + @media (max-width: 800px){ fieldset.registrar-fieldset__contact { padding: 0; From 04ced13eb6bc4468e40b22cbf9d8503058749f95 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 12:08:53 -0600 Subject: [PATCH 29/66] Minor tweaks --- src/registrar/assets/js/get-gov.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index d18129456..315ff38f4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -890,7 +890,7 @@ function hideDeletedForms() { // Update the icon reference to the info icon if (parts.length > 1) { - parts[1] = "info"; + parts[1] = "info_outline"; useElement.setAttribute("xlink:href", parts.join("#")); // Change the color to => $dhs-dark-gray-60 @@ -938,6 +938,7 @@ function hideDeletedForms() { } function showInputOnErrorFields(){ + let fullNameButtonClicked = false document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { let fieldIdParts = button.id.split("__") @@ -947,18 +948,26 @@ function hideDeletedForms() { let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { let nameFields = ["first_name", "middle_name", "last_name"] + + button.click() + // If either the full_name field errors out, // or if any of its associated fields do - show all name related fields. // Otherwise, just show the problematic field. - if (fieldName == "full_name"){ - handleFullNameField(fieldName); - }else if (nameFields.includes(fieldName)){ - handleFullNameField("full_name"); - button.click() - } - else { - button.click() + if (nameFields.includes(fieldName) && !fullNameButtonClicked){ + fullNameButton = document.querySelector("#full_name__edit-button") + if (fullNameButton) { + fullNameButton.click() + fullNameButtonClicked = true + } + + let readonlyId = getReadonlyFieldId("full_name"); + let readonlyField = document.querySelector(readonlyId); + if (readonlyField) { + readonlyField.classList.toggle("overlapped-full-name-field"); + } } + } } }); From bb42732c80a2cf0f018480da47a14d0e6d151432 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 15:24:19 -0600 Subject: [PATCH 30/66] Cleanup Pushing old commit content to save work already done --- src/registrar/config/urls.py | 4 +- src/registrar/forms/domain.py | 1 - .../{contact.py => finish_user_setup.py} | 4 +- .../forms/utility/wizard_form_helper.py | 1 - .../models/utility/generic_helper.py | 27 +- src/registrar/registrar_middleware.py | 68 ++-- src/registrar/views/__init__.py | 4 +- .../{contact.py => finish_user_setup.py} | 323 ++++++++---------- src/registrar/views/utility/mixins.py | 14 +- 9 files changed, 211 insertions(+), 235 deletions(-) rename src/registrar/forms/{contact.py => finish_user_setup.py} (94%) rename src/registrar/views/{contact.py => finish_user_setup.py} (63%) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 596e5c3d2..215e1740e 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -104,8 +104,8 @@ urlpatterns = [ # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. "finish-user-setup/", - views.ContactProfileSetupView.as_view(), - name="finish-contact-profile-setup", + views.FinishUserSetupView.as_view(), + name="finish-user-profile-setup", ), path( "domain-request//edit/", diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index db247ad21..da1462bdb 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -203,7 +203,6 @@ NameserverFormset = formset_factory( ) -# TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/finish_user_setup.py similarity index 94% rename from src/registrar/forms/contact.py rename to src/registrar/forms/finish_user_setup.py index 9a752bab4..46b75090f 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/finish_user_setup.py @@ -2,8 +2,8 @@ from django import forms from phonenumber_field.formfields import PhoneNumberField # type: ignore -class ContactForm(forms.Form): - """Form for adding or editing a contact""" +class FinishUserSetupForm(forms.Form): + """Form for adding or editing user information""" def clean(self): cleaned_data = super().clean() diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2dd1a2b42..2ae50f908 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -22,7 +22,6 @@ class RegistrarForm(forms.Form): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object self.domain_request = kwargs.pop("domain_request", None) - super(RegistrarForm, self).__init__(*args, **kwargs) def to_database(self, obj: DomainRequest | Contact): diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 861fcea07..5f92b5953 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,7 +2,8 @@ import time import logging - +from typing import Any +from urllib.parse import urlparse, urlunparse, urlencode logger = logging.getLogger(__name__) @@ -286,3 +287,27 @@ def from_database(form_class, obj): if obj is None: return {} return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore + + +def replace_url_queryparams(url_to_modify: str, query_params: dict[Any, list]): + """ + Replaces the query parameters of a given URL. + + Args: + url_to_modify (str): The URL whose query parameters need to be modified. + query_params (dict): Dictionary of query parameters to use. + + Returns: + str: The modified URL with the updated query parameters. + """ + + # Split the URL into parts + url_parts = list(urlparse(url_to_modify)) + + # Modify the query param bit + url_parts[4] = urlencode(query_params) + + # Reassemble the URL + new_url = urlunparse(url_parts) + + return new_url \ No newline at end of file diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 8dca06019..5707c4d5d 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -7,6 +7,8 @@ from django.urls import reverse from django.http import HttpResponseRedirect from waffle.decorators import flag_is_active +from registrar.models.utility.generic_helper import replace_url_queryparams + class CheckUserProfileMiddleware: """ @@ -28,49 +30,47 @@ class CheckUserProfileMiddleware: # Check that the user is "opted-in" to the profile feature flag has_profile_feature_flag = flag_is_active(request, "profile_feature") - # If they aren't, skip this entirely + # If they aren't, skip this check entirely if not has_profile_feature_flag: return None # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) - logout_page = reverse("logout") - excluded_pages = [ - setup_page, - logout_page, - ] - custom_redirect = None - - # In some cases, we don't want to redirect to home. - # This handles that. - if request.path == "/request/": - # This can be generalized if need be, but for now lets keep this easy to read. - custom_redirect = "domain-request:" - - # Don't redirect on excluded pages (such as the setup page itself) - if not any(request.path.startswith(page) for page in excluded_pages): - # Preserve the original query parameters, and coerce them into a dict - query_params = parse_qs(request.META["QUERY_STRING"]) - - if custom_redirect is not None: - # Set the redirect value to our redirect location - query_params["redirect"] = custom_redirect - - if query_params: - # Split the URL into parts - setup_page_parts = list(urlparse(setup_page)) - # Modify the query param bit - setup_page_parts[4] = urlencode(query_params) - # Reassemble the URL - setup_page = urlunparse(setup_page_parts) - - # Redirect to the setup page - return HttpResponseRedirect(setup_page) + return self._handle_setup_not_finished(request) # Continue processing the view return None + + def _handle_setup_not_finished(self, request): + setup_page = reverse("finish-user-profile-setup", kwargs={"pk": request.user.contact.pk}) + logout_page = reverse("logout") + excluded_pages = [ + setup_page, + logout_page, + ] + + # In some cases, we don't want to redirect to home. This handles that. + # Can easily be generalized if need be, but for now lets keep this easy to read. + custom_redirect = "domain-request:" if request.path == "/request/" else None + + # Don't redirect on excluded pages (such as the setup page itself) + if not any(request.path.startswith(page) for page in excluded_pages): + # Preserve the original query parameters, and coerce them into a dict + query_params = parse_qs(request.META["QUERY_STRING"]) + + if custom_redirect is not None: + # Set the redirect value to our redirect location + query_params["redirect"] = custom_redirect + + if query_params: + setup_page = replace_url_queryparams(setup_page, query_params) + + # Redirect to the setup page + return HttpResponseRedirect(setup_page) + else: + # Process the view as normal + return None class NoCacheMiddleware: diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 692cfd4de..961d0d94b 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -14,8 +14,8 @@ from .domain import ( DomainInvitationDeleteView, DomainDeleteUserView, ) -from .contact import ( - ContactProfileSetupView, +from .finish_user_setup import ( + FinishUserSetupView, ) from .health import * from .index import * diff --git a/src/registrar/views/contact.py b/src/registrar/views/finish_user_setup.py similarity index 63% rename from src/registrar/views/contact.py rename to src/registrar/views/finish_user_setup.py index 257f0db01..f545b1be9 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/finish_user_setup.py @@ -1,13 +1,14 @@ +from enum import Enum from waffle.decorators import waffle_flag -from urllib.parse import urlencode, urlunparse, urlparse, quote +from urllib.parse import quote from django.urls import NoReverseMatch, reverse -from registrar.forms.contact import ContactForm +from registrar.forms.finish_user_setup import FinishUserSetupForm from django.contrib.messages.views import SuccessMessageMixin from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin -from registrar.models.utility.generic_helper import to_database, from_database +from registrar.models.utility.generic_helper import replace_url_queryparams, to_database, from_database from django.utils.safestring import mark_safe from django.utils.decorators import method_decorator @@ -19,27 +20,17 @@ logger = logging.getLogger(__name__) class BaseContactView(SuccessMessageMixin, ContactPermissionView): - """Provides a base view for the contact object. On get, the contact - is saved in the session and on self.object.""" def get_success_message(self, cleaned_data): """Content of the returned success message""" return "Contact updated successfully." def get(self, request, *args, **kwargs): - """Sets the current contact in cache, defines the current object as self.object - then returns render_to_response""" - self._set_contact(request) + self._update_object_and_session(request) context = self.get_context_data(object=self.object) return self.render_to_response(context) - def _set_contact(self, request): - """ - get contact from session cache or from db and set - to self.object - set session to self for downstream functions to - update session cache - """ + def _update_object_and_session(self, request): self.session = request.session contact_pk = "contact:" + str(self.kwargs.get("pk")) @@ -50,9 +41,9 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): else: self.object = self.get_object() - self._update_session_with_contact() + self._refresh_session() - def _update_session_with_contact(self): + def _refresh_session(self): """ Set contact pk in the session cache """ @@ -63,46 +54,30 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): """Adds a FormMixin to BaseContactView, and handles post""" - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using BaseContactView and FormMixin - """ - # Set the current contact object in cache - self._set_contact(request) - - form = self.get_form() - - # Get the current form and validate it - return self.form_valid(form) if form.is_valid() else self.form_invalid(form) - def form_invalid(self, form): # updates session cache with contact - self._update_session_with_contact() + self._refresh_session() # superclass has the redirect return super().form_invalid(form) -class ContactProfileSetupView(ContactFormBaseView): +class FinishUserSetupView(ContactFormBaseView): """This view forces the user into providing additional details that we may have missed from Login.gov""" template_name = "finish_contact_setup.html" - form_class = ContactForm + form_class = FinishUserSetupForm model = Contact redirect_type = None - # TODO - make this an enum - class RedirectType: + class RedirectType(Enum): """ - Contains constants for each type of redirection. - Not an enum as we just need to track string values, - but we don't care about enforcing it. + Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. - HOME: We want to redirect to reverse("home") - - BACK_TO_SELF: We want to redirect back to reverse("finish-contact-profile-setup") + - BACK_TO_SELF: We want to redirect back to reverse("finish-user-profile-setup") - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE @@ -113,146 +88,6 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" - def get_success_message(self, cleaned_data): - """Content of the returned success message""" - return "Your profile has been successfully updated." - - # TODO - refactor - @waffle_flag("profile_feature") - @method_decorator(csrf_protect) - def dispatch(self, request, *args, **kwargs): - """ - Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - - This method sets the redirect type based on the 'redirect' query parameter, - defaulting to BACK_TO_SELF if not provided. - It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. - - Returns: - HttpResponse: The response generated by the parent class's dispatch method. - """ - # Default redirect type - default_redirect = self.RedirectType.BACK_TO_SELF - - # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", None) - - is_default = False - # We set this here rather than in .get so we don't override - # existing data if no queryparam is present. - if redirect_type is None: - is_default = True - redirect_type = default_redirect - - # Set the default if nothing exists already - if self.redirect_type is None: - self.redirect_type = redirect_type - - if not is_default: - default_redirects = [ - self.RedirectType.HOME, - self.RedirectType.COMPLETE_SETUP, - self.RedirectType.BACK_TO_SELF, - self.RedirectType.TO_SPECIFIC_PAGE, - ] - if redirect_type not in default_redirects: - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - request.session["profile_setup_redirect_viewname"] = redirect_type - else: - self.redirect_type = redirect_type - - return super().dispatch(request, *args, **kwargs) - - def get_redirect_url(self): - """ - Returns a URL string based on the current value of self.redirect_type. - - Depending on self.redirect_type, constructs a base URL and appends a - 'redirect' query parameter. Handles different redirection types such as - HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. - - Returns: - str: The full URL with the appropriate query parameters. - """ - base_url = "" - query_params = {} - match self.redirect_type: - case self.RedirectType.HOME: - base_url = reverse("home") - case self.RedirectType.BACK_TO_SELF | self.RedirectType.COMPLETE_SETUP: - base_url = reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - case self.RedirectType.TO_SPECIFIC_PAGE: - - # We only allow this session value to use viewnames, - # because otherwise this allows anyone to enter any value in here. - # This restricts what can be redirected to. - try: - desired_view = self.session["profile_setup_redirect_viewname"] - base_url = reverse(desired_view) - except NoReverseMatch as err: - logger.error(err) - logger.error("ContactProfileSetupView -> get_redirect_url -> Could not find specified page.") - base_url = reverse("home") - case _: - base_url = reverse("home") - - # Quote cleans up the value so that it can be used in a url - query_params["redirect"] = quote(self.redirect_type) - - # Parse the base URL - url_parts = list(urlparse(base_url)) - - # Update the query part of the URL - url_parts[4] = urlencode(query_params) - - # Construct the full URL with query parameters - full_url = urlunparse(url_parts) - return full_url - - def get_success_url(self): - """Redirect to the nameservers page for the domain.""" - redirect_url = self.get_redirect_url() - return redirect_url - - # TODO - delete session information - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using BaseContactView and FormMixin - """ - - # Set the current contact object in cache - self._set_contact(request) - - form = self.get_form() - - # Get the current form and validate it - if form.is_valid(): - if "contact_setup_save_button" in request.POST: - # Logic for when the 'Save' button is clicked - self.redirect_type = self.RedirectType.COMPLETE_SETUP - elif "contact_setup_submit_button" in request.POST: - if "profile_setup_redirect_viewname" in self.session: - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - else: - self.redirect_type = self.RedirectType.HOME - - return self.form_valid(form) - else: - return self.form_invalid(form) - - def form_valid(self, form): - - completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] - if self.redirect_type in completed_states: - self.request.user.finished_setup = True - self.request.user.save() - - to_database(form=form, obj=self.object) - self._update_session_with_contact() - - return super().form_valid(form) - def get_initial(self): """The initial value for the form (which is a formset here).""" db_object = from_database(form_class=self.form_class, obj=self.object) @@ -276,3 +111,133 @@ class ContactProfileSetupView(ContactFormBaseView): "If the wrong email is displayed below, you’ll need to update your Login.gov account " f'and log back in. Get help with your Login.gov account.' ) # nosec + + + def get_success_message(self, cleaned_data): + """Content of the returned success message""" + return "Your profile has been successfully updated." + + @waffle_flag("profile_feature") + @method_decorator(csrf_protect) + def dispatch(self, request, *args, **kwargs): + """ + Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. + + This method sets the redirect type based on the 'redirect' query parameter, + defaulting to BACK_TO_SELF if not provided. + It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + + Returns: + HttpResponse: The response generated by the parent class's dispatch method. + """ + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", None) + + # We set this here rather than in .get so we don't override + # existing data if no queryparam is present. + is_default = redirect_type is None + if is_default: + # Set to the default + redirect_type = self.RedirectType.BACK_TO_SELF + self.redirect_type = redirect_type + else: + all_redirect_types = [r.value for r in self.RedirectType] + if redirect_type in all_redirect_types: + self.redirect_type = self.RedirectType(redirect_type) + else: + # If the redirect type is undefined, then we assume that + # we are specifying a particular page to redirect to. + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + + # Store the page that we want to redirect to for later use + request.session["redirect_viewname"] = str(redirect_type) + + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + + # Set the current object in cache + self._update_object_and_session(request) + + form = self.get_form() + + # Get the current form and validate it + if form.is_valid(): + if "contact_setup_save_button" in request.POST: + # Logic for when the 'Save' button is clicked + self.redirect_type = self.RedirectType.COMPLETE_SETUP + elif "contact_setup_submit_button" in request.POST: + if "redirect_viewname" in self.session: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + else: + self.redirect_type = self.RedirectType.HOME + + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """Saves the current contact to the database, and if the user is complete + with their setup, then we mark user.finished_setup to True.""" + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] + if self.redirect_type in completed_states: + self.request.user.finished_setup = True + self.request.user.save() + + to_database(form=form, obj=self.object) + self._refresh_session() + + return super().form_valid(form) + + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + redirect_url = self.get_redirect_url() + return redirect_url + + def get_redirect_url(self): + """ + Returns a URL string based on the current value of self.redirect_type. + + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as + HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. + + Returns: + str: The full URL with the appropriate query parameters. + """ + + # These redirect types redirect to the same page + self_redirect = [ + self.RedirectType.BACK_TO_SELF, + self.RedirectType.COMPLETE_SETUP + ] + + # Maps the redirect type to a URL + base_url = "" + try: + if self.redirect_type in self_redirect: + base_url = reverse("finish-user-profile-setup", kwargs={"pk": self.object.pk}) + elif self.redirect_type == self.RedirectType.TO_SPECIFIC_PAGE: + # We only allow this session value to use viewnames, + # because this restricts what can be redirected to. + desired_view = self.session["redirect_viewname"] + base_url = reverse(desired_view) + else: + base_url = reverse("home") + except NoReverseMatch as err: + logger.error(f"get_redirect_url -> Could not find the specified page. Err: {err}") + + query_params = {} + + # Quote cleans up the value so that it can be used in a url + query_params["redirect"] = quote(self.redirect_type.value) + + # Generate the full url from the given query params + full_url = replace_url_queryparams(base_url, query_params) + return full_url + diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 3e5e10816..6f5c08b98 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -358,19 +358,7 @@ class ContactPermission(PermissionsLoginMixin): if not requested_user_exists or not requested_contact_exists: return False - # Check if the user has an associated contact - associated_contacts = Contact.objects.filter(user=current_user) - associated_contacts_length = len(associated_contacts) - - if associated_contacts_length == 0: - # This means that the user trying to access this page - # is a different user than the contact holder. - return False - elif associated_contacts_length > 1: - # TODO - change this - raise ValueError("User has multiple connected contacts") - else: - return True + return True class DomainRequestPermissionWithdraw(PermissionsLoginMixin): From 5ed0a56d79b997ba956f271557ee41a615550f85 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 16 May 2024 09:50:08 -0600 Subject: [PATCH 31/66] Fix migration --- ...{0094_user_finished_setup.py => 0095_user_finished_setup.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/registrar/migrations/{0094_user_finished_setup.py => 0095_user_finished_setup.py} (83%) diff --git a/src/registrar/migrations/0094_user_finished_setup.py b/src/registrar/migrations/0095_user_finished_setup.py similarity index 83% rename from src/registrar/migrations/0094_user_finished_setup.py rename to src/registrar/migrations/0095_user_finished_setup.py index 660f950c0..87e247330 100644 --- a/src/registrar/migrations/0094_user_finished_setup.py +++ b/src/registrar/migrations/0095_user_finished_setup.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0093_alter_publiccontact_unique_together"), + ("registrar", "0094_create_groups_v12"), ] operations = [ From 9d576ed1cbbe24f0e225cedcdd9c06a92c054326 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 20 May 2024 10:15:53 -0600 Subject: [PATCH 32/66] Simplify --- src/registrar/views/finish_user_setup.py | 32 +++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/registrar/views/finish_user_setup.py b/src/registrar/views/finish_user_setup.py index f545b1be9..75eddb011 100644 --- a/src/registrar/views/finish_user_setup.py +++ b/src/registrar/views/finish_user_setup.py @@ -132,26 +132,18 @@ class FinishUserSetupView(ContactFormBaseView): """ # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", None) + redirect_type = request.GET.get("redirect", self.RedirectType.BACK_TO_SELF) - # We set this here rather than in .get so we don't override - # existing data if no queryparam is present. - is_default = redirect_type is None - if is_default: - # Set to the default - redirect_type = self.RedirectType.BACK_TO_SELF - self.redirect_type = redirect_type + all_redirect_types = [r.value for r in self.RedirectType] + if redirect_type in all_redirect_types: + self.redirect_type = self.RedirectType(redirect_type) else: - all_redirect_types = [r.value for r in self.RedirectType] - if redirect_type in all_redirect_types: - self.redirect_type = self.RedirectType(redirect_type) - else: - # If the redirect type is undefined, then we assume that - # we are specifying a particular page to redirect to. - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + # If the redirect type is undefined, then we assume that + # we are specifying a particular page to redirect to. + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - # Store the page that we want to redirect to for later use - request.session["redirect_viewname"] = str(redirect_type) + # Store the page that we want to redirect to for later use + request.session["redirect_viewname"] = str(redirect_type) return super().dispatch(request, *args, **kwargs) @@ -168,6 +160,12 @@ class FinishUserSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): + + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] + if self.redirect_type in completed_states: + self.request.user.finished_setup = True + self.request.user.save() + if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked self.redirect_type = self.RedirectType.COMPLETE_SETUP From 9a6ccc1c44d136ff12e1a2ff910f705dd3b5f266 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 20 May 2024 11:25:32 -0600 Subject: [PATCH 33/66] Replace form with modelform Pull in content from related PR and refactor around it --- src/registrar/assets/js/get-gov.js | 29 +-- .../assets/sass/_theme/_fieldsets.scss | 23 +- src/registrar/config/urls.py | 4 +- src/registrar/forms/finish_user_setup.py | 48 ---- src/registrar/forms/user_profile.py | 30 ++- src/registrar/models/contact.py | 7 - ...t_setup.html => finish_profile_setup.html} | 18 +- src/registrar/views/__init__.py | 5 +- src/registrar/views/finish_user_setup.py | 241 ------------------ src/registrar/views/user_profile.py | 188 +++++++++++++- 10 files changed, 230 insertions(+), 363 deletions(-) delete mode 100644 src/registrar/forms/finish_user_setup.py rename src/registrar/templates/{finish_contact_setup.html => finish_profile_setup.html} (82%) delete mode 100644 src/registrar/views/finish_user_setup.py diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 315ff38f4..2fd534f04 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -873,35 +873,16 @@ function hideDeletedForms() { let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#contact-full-name-fieldset"); + let nameFieldset = document.querySelector("#profile-name-fieldset"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } if (inputField) { - let readonlyId = getReadonlyFieldId(fieldName) - let readonlyField = document.querySelector(readonlyId) - if (readonlyField) { - // Update the element's xlink:href attribute - let useElement = readonlyField.querySelector("use"); - if (useElement) { - let currentHref = useElement.getAttribute("xlink:href"); - let parts = currentHref.split("#"); - - // Update the icon reference to the info icon - if (parts.length > 1) { - parts[1] = "info_outline"; - useElement.setAttribute("xlink:href", parts.join("#")); - - // Change the color to => $dhs-dark-gray-60 - useElement.closest('svg').style.fill = '#444547'; - } - } - - let parentDiv = readonlyField.closest("div"); - if (parentDiv) { - parentDiv.classList.toggle("overlapped-full-name-field"); - } + // Remove the "full_name" field + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.remove(); } } } diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index da900f128..7ad0a2a82 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -10,23 +10,8 @@ fieldset:not(:first-child) { } fieldset.registrar-fieldset__contact { - border-width: 2px; - border-left: none; - border-right: none; - border-bottom: none; - padding-bottom: 0; + // This fieldset is for SR purposes only + border: 0; + margin: 0; + padding: 0; } - -@media (min-width: 800px) { - fieldset.registrar-fieldset__contact { - margin-top: 28px; - } -} - -@media (max-width: 800px){ - fieldset.registrar-fieldset__contact { - padding: 0; - margin: 0; - border: none; - } -} \ No newline at end of file diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 5ed9adae0..2d4c33569 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -103,8 +103,8 @@ urlpatterns = [ path( # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. - "finish-user-setup/", - views.FinishUserSetupView.as_view(), + "finish-profile-setup/", + views.FinishProfileSetupView.as_view(), name="finish-user-profile-setup", ), path( diff --git a/src/registrar/forms/finish_user_setup.py b/src/registrar/forms/finish_user_setup.py deleted file mode 100644 index 46b75090f..000000000 --- a/src/registrar/forms/finish_user_setup.py +++ /dev/null @@ -1,48 +0,0 @@ -from django import forms -from phonenumber_field.formfields import PhoneNumberField # type: ignore - - -class FinishUserSetupForm(forms.Form): - """Form for adding or editing user information""" - - def clean(self): - cleaned_data = super().clean() - # Remove the full name property - if "full_name" in cleaned_data: - # Delete the full name element as its purely decorative. - # We include it as a normal Charfield for all the advantages - # and utility that it brings, but we're playing pretend. - del cleaned_data["full_name"] - return cleaned_data - - full_name = forms.CharField( - label="Full name", - error_messages={"required": "Enter your full name"}, - ) - first_name = forms.CharField( - label="First name / given name", - error_messages={"required": "Enter your first name / given name."}, - ) - middle_name = forms.CharField( - required=False, - label="Middle name (optional)", - ) - last_name = forms.CharField( - label="Last name / family name", - error_messages={"required": "Enter your last name / family name."}, - ) - title = forms.CharField( - label="Title or role in your organization", - error_messages={ - "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") - }, - ) - email = forms.EmailField( - label="Organization email", - required=False, - max_length=None, - ) - phone = PhoneNumberField( - label="Phone", - error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, - ) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 036b03751..abc90d195 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -55,6 +55,34 @@ class UserProfileForm(forms.ModelForm): "required": "Enter your email address in the required format, like name@example.com." } self.fields["phone"].error_messages["required"] = "Enter your phone number." - self.domainInfo = None DomainHelper.disable_field(self.fields["email"], disable_required=True) + + +class FinishSetupProfileForm(UserProfileForm): + """Form for updating user profile.""" + + full_name = forms.CharField(required=True, label="Full name") + + def clean(self): + cleaned_data = super().clean() + # Remove the full name property + if "full_name" in cleaned_data: + # Delete the full name element as its purely decorative. + # We include it as a normal Charfield for all the advantages + # and utility that it brings, but we're playing pretend. + del cleaned_data["full_name"] + return cleaned_data + + def __init__(self, *args, **kwargs): + """Override the inerited __init__ method to update the fields.""" + + super().__init__(*args, **kwargs) + + # Set custom form label for email + self.fields["email"].label = "Organization email" + self.fields["title"].label = "Title or role in your organization" + + # Define the "full_name" value + if self.instance and hasattr(self.instance, 'full_name'): + self.fields["full_name"].initial = self.instance.get_formatted_name() diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 716849b32..0f55ed863 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -102,13 +102,6 @@ class Contact(TimeStampedModel): names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] return " ".join(names) if names else "Unknown" - @property - def full_name(self): - """ - Returns the full name (first_name, middle_name, last_name) of this contact. - """ - return self.get_formatted_name() - def has_contact_info(self): return bool(self.title or self.email or self.phone) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_profile_setup.html similarity index 82% rename from src/registrar/templates/finish_contact_setup.html rename to src/registrar/templates/finish_profile_setup.html index 301f5837a..a8e16724e 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -62,25 +62,17 @@ Your contact information - - {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} - {# Also todo: consolidate all of the scattered classes into this usa form one #} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} - -{% endblock profile_form %} \ No newline at end of file +{% endblock profile_form %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6215d1fdc..045641ef9 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -523,6 +523,10 @@ class HomeTests(TestWithUser): class FinishUserProfileTests(TestWithUser, WebTest): """A series of tests that target the finish setup page for user profile""" + # csrf checks do not work well with WebTest. + # We disable them here. + csrf_checks = False + def setUp(self): super().setUp() self.user.title = None @@ -556,7 +560,8 @@ class FinishUserProfileTests(TestWithUser, WebTest): """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) 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. finish_setup_page = self.app.get(reverse("home")).follow() self._set_session_cookie() @@ -578,10 +583,14 @@ class FinishUserProfileTests(TestWithUser, WebTest): finish_setup_form["phone"] = "(201) 555-0123" finish_setup_form["title"] = "CEO" finish_setup_form["last_name"] = "example" - completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True) + save_page = self._submit_form_webtest(finish_setup_form, follow=True) - self.assertEqual(completed_setup_page.status_code, 200) - # Assert that we're on the home page + self.assertEqual(save_page.status_code, 200) + self.assertContains(save_page, "Your profile has been updated.") + + # 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") @less_console_noise_decorator From 32135ef4b917bace506cef26bee80a1fbef0d7b1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 21 May 2024 15:55:13 -0600 Subject: [PATCH 49/66] Cleanup js --- src/djangooidc/backends.py | 1 - src/djangooidc/views.py | 2 -- src/registrar/assets/js/get-gov.js | 6 +----- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 25179f7fd..41e442f2d 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -23,7 +23,6 @@ class OpenIdConnectBackend(ModelBackend): def authenticate(self, request, **kwargs): logger.debug("kwargs %s" % kwargs) user = None - if not kwargs or "sub" not in kwargs.keys(): return user diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index bbf708d1c..815df4ecf 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -113,12 +113,10 @@ def login_callback(request): user.save() login(request, user) - logger.info("Successfully logged in user %s" % user) # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2e536c973..2958399ec 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -920,11 +920,9 @@ function hideDeletedForms() { function showInputOnErrorFields(){ document.addEventListener('DOMContentLoaded', function() { - let form = document.querySelector("#finish-profile-setup-form"); - console.log(`form: ${form}`) // Get all input elements within the form + let form = document.querySelector("#finish-profile-setup-form"); let inputs = form ? form.querySelectorAll("input") : null; - console.log(`look: ${inputs}`) if (!inputs) { return null; } @@ -933,13 +931,11 @@ function hideDeletedForms() { inputs.forEach(function(input) { let fieldName = input.name; let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); - console.log(`fieldName: ${inputs} vs err message ${errorMessage}`) if (!fieldName || !errorMessage) { return null; } let editButton = document.querySelector(`#${fieldName}__edit-button`); - console.log(`edit button is ${editButton} vs id #${fieldName}__edit-button`) if (editButton){ // Show the input field of the field that errored out editButton.click(); From 373538c78a8976484c1994c7c4e768ea1589e783 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 22 May 2024 10:30:47 -0600 Subject: [PATCH 50/66] Cleanup --- .../templates/finish_profile_setup.html | 4 +- src/registrar/tests/test_views.py | 40 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html index d1bae6fa3..4b087124d 100644 --- a/src/registrar/templates/finish_profile_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -5,7 +5,7 @@ {# Disable the redirect #} {% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=False %} + {% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %} {% endblock %} {# Add the new form #} @@ -16,5 +16,5 @@ {% endblock content_bottom %} {% block footer %} - {% include "includes/footer.html" %} + {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %} {% endblock footer %} \ No newline at end of file diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 045641ef9..4a73e4c8b 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -55,9 +55,13 @@ class TestWithUser(MockEppLib): first_name = "First" last_name = "Last" email = "info@example.com" + phone = "8003111234" self.user = get_user_model().objects.create( - username=username, first_name=first_name, last_name=last_name, email=email + username=username, first_name=first_name, last_name=last_name, email=email, phone=phone ) + title="test title" + self.user.contact.title = title + self.user.save() username_incomplete = "test_user_incomplete" first_name_2 = "Incomplete" @@ -671,7 +675,7 @@ class UserProfileTests(TestWithUser, WebTest): assume that the same test results hold true for 401 and 403.""" with override_flag("profile_feature", active=True): with self.assertRaises(Exception): - response = self.client.get(reverse("home")) + response = self.client.get(reverse("home"), follow=True) self.assertEqual(response.status_code, 500) self.assertContains(response, "Your profile") @@ -691,49 +695,49 @@ class UserProfileTests(TestWithUser, WebTest): def test_home_page_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of home page when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/") + response = self.client.get("/", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_home_page_main_nav_with_profile_feature_off(self): """test that Your profile is not in main nav of home page when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/") + response = self.client.get("/", follow=True) self.assertNotContains(response, "Your profile") @less_console_noise_decorator def test_new_request_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of new request when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/request/") + response = self.client.get("/request/", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_new_request_main_nav_with_profile_feature_off(self): """test that Your profile is not in main nav of new request when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/request/") + response = self.client.get("/request/", follow=True) self.assertNotContains(response, "Your profile") @less_console_noise_decorator def test_user_profile_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of user profile when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/user-profile") + response = self.client.get("/user-profile", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_user_profile_returns_404_when_feature_off(self): """test that Your profile returns 404 when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/user-profile") + response = self.client.get("/user-profile", follow=True) self.assertEqual(response.status_code, 404) @less_console_noise_decorator def test_domain_detail_profile_feature_on(self): """test that domain detail view when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(reverse("domain", args=[self.domain.pk])) + response = self.client.get(reverse("domain", args=[self.domain.pk]), follow=True) self.assertContains(response, "Your profile") self.assertNotContains(response, "Your contact information") @@ -741,14 +745,14 @@ class UserProfileTests(TestWithUser, WebTest): def test_domain_your_contact_information_when_profile_feature_off(self): """test that Your contact information is accessible when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get(f"/domain/{self.domain.id}/your-contact-information") + response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True) self.assertContains(response, "Your contact information") @less_console_noise_decorator def test_domain_your_contact_information_when_profile_feature_on(self): """test that Your contact information is not accessible when profile feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(f"/domain/{self.domain.id}/your-contact-information") + response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True) self.assertEqual(response.status_code, 404) @less_console_noise_decorator @@ -765,9 +769,9 @@ class UserProfileTests(TestWithUser, WebTest): submitter=contact_user, ) with override_flag("profile_feature", active=True): - response = self.client.get(f"/domain-request/{domain_request.id}") + response = self.client.get(f"/domain-request/{domain_request.id}", follow=True) self.assertContains(response, "Your profile") - response = self.client.get(f"/domain-request/{domain_request.id}/withdraw") + response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True) self.assertContains(response, "Your profile") # cleanup domain_request.delete() @@ -787,9 +791,9 @@ class UserProfileTests(TestWithUser, WebTest): submitter=contact_user, ) with override_flag("profile_feature", active=False): - response = self.client.get(f"/domain-request/{domain_request.id}") + response = self.client.get(f"/domain-request/{domain_request.id}", follow=True) self.assertNotContains(response, "Your profile") - response = self.client.get(f"/domain-request/{domain_request.id}/withdraw") + response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True) self.assertNotContains(response, "Your profile") # cleanup domain_request.delete() @@ -800,16 +804,14 @@ class UserProfileTests(TestWithUser, WebTest): """test user profile form submission""" self.app.set_user(self.user.username) with override_flag("profile_feature", active=True): - profile_page = self.app.get(reverse("user-profile")) + profile_page = self.app.get(reverse("user-profile")).follow() session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) profile_form = profile_page.form profile_page = profile_form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # assert that first result contains errors - self.assertContains(profile_page, "Enter your title") - self.assertContains(profile_page, "Enter your phone number") + profile_form = profile_page.form profile_form["title"] = "sample title" profile_form["phone"] = "(201) 555-1212" From 03afa329c5544b39dde9a41de187c7b625af3382 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 22 May 2024 11:45:48 -0600 Subject: [PATCH 51/66] Cleanup --- src/registrar/tests/test_views.py | 2 +- src/registrar/views/domain_request.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 4a73e4c8b..595544526 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -59,7 +59,7 @@ class TestWithUser(MockEppLib): self.user = get_user_model().objects.create( username=username, first_name=first_name, last_name=last_name, email=email, phone=phone ) - title="test title" + title = "test title" self.user.contact.title = title self.user.save() diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index a90eaf271..67e01e5be 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -15,6 +15,7 @@ from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView +from waffle.decorators import flag_is_active, waffle_flag from .utility import ( DomainRequestPermissionView, @@ -400,7 +401,15 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] + excluded_steps = [ + Step.YOUR_CONTACT + ] + should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: + + if should_exclude and step in excluded_steps: + continue + condition = self.WIZARD_CONDITIONS.get(step, True) if callable(condition): condition = condition(self) @@ -540,6 +549,10 @@ class YourContact(DomainRequestWizard): template_name = "domain_request_your_contact.html" forms = [forms.YourContactForm] + @waffle_flag("!profile_feature") # type: ignore + def dispatch(self, request, *args, **kwargs): # type: ignore + return super().dispatch(request, *args, **kwargs) + class OtherContacts(DomainRequestWizard): template_name = "domain_request_other_contacts.html" From 1fd86875c04fadc1f48944cfdf3791f1ef887586 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 23 May 2024 09:33:34 -0600 Subject: [PATCH 52/66] Linting --- src/registrar/tests/test_views.py | 4 ++-- src/registrar/views/domain_request.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 595544526..5275b8314 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -61,7 +61,7 @@ class TestWithUser(MockEppLib): ) title = "test title" self.user.contact.title = title - self.user.save() + self.user.contact.save() username_incomplete = "test_user_incomplete" first_name_2 = "Incomplete" @@ -737,7 +737,7 @@ class UserProfileTests(TestWithUser, WebTest): def test_domain_detail_profile_feature_on(self): """test that domain detail view when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(reverse("domain", args=[self.domain.pk]), follow=True) + response = self.client.get(reverse("domain", args=[self.domain.pk])) self.assertContains(response, "Your profile") self.assertNotContains(response, "Your contact information") diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 67e01e5be..1d14e4b57 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -23,7 +23,6 @@ from .utility import ( DomainRequestWizardPermissionView, ) -from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -401,9 +400,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] - excluded_steps = [ - Step.YOUR_CONTACT - ] + excluded_steps = [Step.YOUR_CONTACT] should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: From 7684fff0afd67226de29c5d00f215399ead072b5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 23 May 2024 10:24:30 -0600 Subject: [PATCH 53/66] Remove unused follow --- src/registrar/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5275b8314..738a0afe2 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -804,7 +804,7 @@ class UserProfileTests(TestWithUser, WebTest): """test user profile form submission""" self.app.set_user(self.user.username) with override_flag("profile_feature", active=True): - profile_page = self.app.get(reverse("user-profile")).follow() + profile_page = self.app.get(reverse("user-profile")) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) profile_form = profile_page.form From 6a28b013f2bf235dd69fd14e560a88c8117f2420 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 08:56:55 -0600 Subject: [PATCH 54/66] Cleanup --- src/registrar/assets/js/get-gov.js | 2 ++ src/registrar/assets/sass/_theme/_base.scss | 10 ------- .../templates/domain_request_intro.html | 29 +------------------ .../templates/finish_profile_setup.html | 2 +- .../includes/finish_profile_form.html | 2 +- src/registrar/views/user_profile.py | 3 +- 6 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2958399ec..5db6cea54 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -931,6 +931,8 @@ function hideDeletedForms() { inputs.forEach(function(input) { let fieldName = input.name; let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); + + // If no error message is found, do nothing if (!fieldName || !errorMessage) { return null; } diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index cd2de3a71..5e7fa0c6c 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -198,13 +198,3 @@ abbr[title] { height: 1.25em !important; } } - -// todo - move this to a better location + better name -@media (min-width: 800px){ - .overlapped-full-name-field { - position: absolute; - top: 337px; - background-color: white; - padding-right: 8px; - } -} diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 73e98ecde..259657ad4 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -13,34 +13,7 @@

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.

Time to complete the form

If you have all the information you need, - completing your domain request might take around 15 minutes.

- -

How we’ll reach you

-

- While reviewing your domain request, we may need to reach out with questions. - We’ll also email you when we complete our review. - If the contact information below is not correct, visit your profile to make updates. -

- -
-
-

- Your contact information -

-
-
    -
  • Full name: {{ user.contact.get_formatted_name }}
  • -
  • Organization email: {{ user.contact.email }}
  • -
  • Title or role in your organization: {{ user.contact.title }}
  • -
  • Phone: {{ user.contact.phone }}
  • -
-
-
-
+ completing your domain request might take around 15 minutes.

{% block form_buttons %}
diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html index 4b087124d..f8070551b 100644 --- a/src/registrar/templates/finish_profile_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -17,4 +17,4 @@ {% block footer %} {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %} -{% endblock footer %} \ No newline at end of file +{% endblock footer %} diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 1d520b64a..9675e204e 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -20,7 +20,7 @@ Note that editing this information won’t affect your Login.gov account information.

-{# We use a var called 'remove_margin_top' rather than 'add_margin_top' because most fields use this #} +{# We use a var called 'remove_margin_top' rather than 'add_margin_top' because this is more useful as a default #} {% include "includes/required_fields.html" with remove_margin_top=True %} {% endblock profile_blurb %} diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index d65de1660..b12812849 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -220,7 +220,8 @@ class FinishProfileSetupView(UserProfileView): query_params = {} # Quote cleans up the value so that it can be used in a url - query_params["redirect"] = quote(self.redirect_type.value) + if self.redirect_type: + query_params["redirect"] = quote(self.redirect_type.value) # Generate the full url from the given query params full_url = replace_url_queryparams(base_url, query_params) From aa875aa325f455fd4418dc2fa07f4b6e35f1618b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 12:02:15 -0600 Subject: [PATCH 55/66] Update profile_form.html --- .../templates/includes/profile_form.html | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index ce337b4d5..f0ec0bc89 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -6,7 +6,7 @@ {% endblock profile_header %} {% block profile_blurb %} -

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

+

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

Contact information

Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

@@ -15,9 +15,9 @@ {% block profile_form %} +
{% csrf_token %} -
{% input_with_errors form.first_name %} @@ -30,22 +30,21 @@ {% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %} {% with link_href=login_help_url %} - {% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %} + {% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %} {% with link_text="Get help with your Login.gov account" %} - {% with target_blank=True %} + {% with target_blank=True %} {% with do_not_show_max_chars=True %} - {% input_with_errors form.email %} - {% endwith %} + {% input_with_errors form.email %} {% endwith %} + {% endwith %} {% endwith %} - {% endwith %} + {% endwith %} {% endwith %} {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone %} + {% input_with_errors form.phone %} {% endwith %} -
- +
{% endblock profile_form %} From a32a228e6ccb8edf9f7a809cd65a26100c284b02 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 13:06:06 -0600 Subject: [PATCH 56/66] Code cleanup --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/assets/sass/_theme/_base.scss | 13 +++++++ src/registrar/assets/sass/_theme/_forms.scss | 9 ++--- .../assets/sass/_theme/_headers.scss | 14 -------- src/registrar/assets/sass/_theme/styles.scss | 1 - .../templates/domain_request_intro.html | 1 + .../includes/finish_profile_form.html | 24 +++++++------ .../templates/includes/gov_extended_logo.html | 2 +- src/registrar/templates/profile.html | 3 +- src/registrar/templatetags/field_helpers.py | 8 +++-- src/registrar/views/domain_request.py | 12 +------ src/registrar/views/user_profile.py | 36 +++++++++++-------- src/registrar/views/utility/mixins.py | 1 - .../views/utility/permission_views.py | 4 ++- 14 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 src/registrar/assets/sass/_theme/_headers.scss diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 5db6cea54..a48d9b46b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -873,7 +873,7 @@ function hideDeletedForms() { let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#profile-name-fieldset"); + let nameFieldset = document.querySelector("#profile-name-group"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 5e7fa0c6c..e88d75f4e 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -198,3 +198,16 @@ abbr[title] { height: 1.25em !important; } } + +// Define some styles for the .gov header/logo +.usa-logo button { + color: #{$dhs-dark-gray-85}; + font-weight: 700; + font-family: family('sans'); + font-size: 1.6rem; + line-height: 1.1; +} + +.usa-logo button.usa-button--unstyled.disabled-button:hover{ + color: #{$dhs-dark-gray-85}; +} diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 46d41059a..b5229fae1 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -15,9 +15,6 @@ .usa-form--extra-large { max-width: none; - .usa-summary-box { - max-width: 600px; - } } .usa-form--text-width { @@ -30,7 +27,7 @@ } } -.usa-form-readonly { +.usa-form-editable { border-top: 2px #{$dhs-dark-gray-15} solid; .bold-usa-label label.usa-label{ @@ -41,14 +38,14 @@ font-weight: bold; } - &.usa-form-readonly--no-border { + &.usa-form-editable--no-border { border-top: None; margin-top: 0px !important; } } -.usa-form-readonly > .usa-form-group:first-of-type { +.usa-form-editable > .usa-form-group:first-of-type { margin-top: unset; } diff --git a/src/registrar/assets/sass/_theme/_headers.scss b/src/registrar/assets/sass/_theme/_headers.scss deleted file mode 100644 index 2e3992e5a..000000000 --- a/src/registrar/assets/sass/_theme/_headers.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use "uswds-core" as *; -@use "cisa_colors" as *; - -.usa-logo button { - color: #{$dhs-dark-gray-85}; - font-weight: 700; - font-family: family('sans'); - font-size: 1.6rem; - line-height: 1.1; -} - -.usa-logo button.usa-button--unstyled.disabled-button:hover{ - color: #{$dhs-dark-gray-85}; -} diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index e24618a23..64b113a29 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -20,7 +20,6 @@ @forward "tables"; @forward "sidenav"; @forward "register-form"; -@forward "_headers"; /*-------------------------------------------------- --- Admin ---------------------------------*/ diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 259657ad4..c0b566799 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -15,6 +15,7 @@

If you have all the information you need, completing your domain request might take around 15 minutes.

+ {% block form_buttons %}
{% endif %} -
\ No newline at end of file +
diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index eeab1f945..12441da66 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -36,5 +36,4 @@ Edit your User Profile | {% include "includes/profile_form.html" with form=form %}
-{% endblock %} - +{% endblock content_bottom %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index a7aa9d663..be78099db 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -95,10 +95,12 @@ def input_with_errors(context, field=None): # noqa: C901 elif key == "show_edit_button": # Hide the primary input field. # Used such that we can toggle it with JS - if "display-none" not in classes and isinstance(value, bool) and value: + if "display-none" not in classes: classes.append("display-none") - # Set this as a context value so we know what we're going to display - context["show_edit_button"] = value + + # Tag that this form contains the edit button. + if "usa-form-editable" not in group_classes: + group_classes.append("usa-form-editable") attrs["id"] = field.auto_id diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 1d14e4b57..a90eaf271 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -15,7 +15,6 @@ from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView -from waffle.decorators import flag_is_active, waffle_flag from .utility import ( DomainRequestPermissionView, @@ -23,6 +22,7 @@ from .utility import ( DomainRequestWizardPermissionView, ) +from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -400,13 +400,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] - excluded_steps = [Step.YOUR_CONTACT] - should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: - - if should_exclude and step in excluded_steps: - continue - condition = self.WIZARD_CONDITIONS.get(step, True) if callable(condition): condition = condition(self) @@ -546,10 +540,6 @@ class YourContact(DomainRequestWizard): template_name = "domain_request_your_contact.html" forms = [forms.YourContactForm] - @waffle_flag("!profile_feature") # type: ignore - def dispatch(self, request, *args, **kwargs): # type: ignore - return super().dispatch(request, *args, **kwargs) - class OtherContacts(DomainRequestWizard): template_name = "domain_request_other_contacts.html" diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index b12812849..a4756f482 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -96,10 +96,6 @@ class FinishProfileSetupView(UserProfileView): """This view forces the user into providing additional details that we may have missed from Login.gov""" - template_name = "finish_profile_setup.html" - form_class = FinishSetupProfileForm - model = Contact - class RedirectType(Enum): """ Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. @@ -116,8 +112,17 @@ class FinishProfileSetupView(UserProfileView): BACK_TO_SELF = "back_to_self" COMPLETE_SETUP = "complete_setup" - redirect_type = None - all_redirect_types = [r.value for r in RedirectType] + @classmethod + def get_all_redirect_types(cls) -> list[str]: + """Returns the value of every redirect type defined in this enum.""" + return [r.value for r in cls] + + template_name = "finish_profile_setup.html" + form_class = FinishSetupProfileForm + model = Contact + + all_redirect_types = RedirectType.get_all_redirect_types() + redirect_type: RedirectType def get_context_data(self, **kwargs): @@ -151,16 +156,18 @@ class FinishProfileSetupView(UserProfileView): """ # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", self.RedirectType.BACK_TO_SELF.value) - if redirect_type in self.all_redirect_types: - self.redirect_type = self.RedirectType(redirect_type) + default_redirect_value = self.RedirectType.BACK_TO_SELF.value + redirect_value = request.GET.get("redirect", default_redirect_value) + + if redirect_value in self.all_redirect_types: + # If the redirect value is a preexisting value in our enum, set it to that. + self.redirect_type = self.RedirectType(redirect_value) else: - # If the redirect type is undefined, then we assume that - # we are specifying a particular page to redirect to. + # If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to. self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE # Store the page that we want to redirect to for later use - request.session["redirect_viewname"] = str(redirect_type) + request.session["redirect_viewname"] = str(redirect_value) return super().dispatch(request, *args, **kwargs) @@ -183,8 +190,7 @@ class FinishProfileSetupView(UserProfileView): def get_success_url(self): """Redirect to the nameservers page for the domain.""" - redirect_url = self.get_redirect_url() - return redirect_url + return self.get_redirect_url() def get_redirect_url(self): """ @@ -220,7 +226,7 @@ class FinishProfileSetupView(UserProfileView): query_params = {} # Quote cleans up the value so that it can be used in a url - if self.redirect_type: + if self.redirect_type and self.redirect_type.value: query_params["redirect"] = quote(self.redirect_type.value) # Generate the full url from the given query params diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 0b8a7605a..926ee4a8c 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -296,7 +296,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): domain_pk = self.kwargs["pk"] user_pk = self.kwargs["user_pk"] - # Check if the user is authenticated if not self.request.user.is_authenticated: return False diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index eb40621b5..d35647af2 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -3,7 +3,9 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView -from registrar.models import Domain, DomainRequest, DomainInvitation, UserDomainRole, Contact +from registrar.models import Domain, DomainRequest, DomainInvitation +from registrar.models.contact import Contact +from registrar.models.user_domain_role import UserDomainRole from .mixins import ( DomainPermission, From e34c72426476ea59ff6e1ef6ea2d28928ae89730 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 13:18:41 -0600 Subject: [PATCH 57/66] Minor refactor --- src/registrar/assets/js/get-gov.js | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index a48d9b46b..baa3f4bbd 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -842,21 +842,14 @@ function hideDeletedForms() { */ (function finishUserSetupListener() { - function getInputFieldId(fieldName){ - return `#id_${fieldName}` - } - - function getReadonlyFieldId(fieldName){ - return `#${fieldName}__edit-button-readonly` + function getInputField(fieldName){ + return document.querySelector(`#id_${fieldName}`) } // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { - let inputId = getInputFieldId(fieldName) - let inputField = document.querySelector(inputId) - - let readonlyId = getReadonlyFieldId(fieldName) - let readonlyField = document.querySelector(readonlyId) + let inputField = getInputField(fieldName) + let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) readonlyField.classList.toggle('display-none'); inputField.classList.toggle('display-none'); @@ -868,18 +861,16 @@ function hideDeletedForms() { } } - function handleFullNameField(fieldName) { + function handleFullNameField(fieldName = "full_name") { // Remove the display-none class from the nearest parent div - let fieldId = getInputFieldId(fieldName) - let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#profile-name-group"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } + // Hide the "full_name" field + let inputField = getInputField(fieldName); if (inputField) { - // Hide the "full_name" field inputFieldParentDiv = inputField.closest("div"); if (inputFieldParentDiv) { inputFieldParentDiv.classList.add("display-none"); @@ -893,11 +884,12 @@ function hideDeletedForms() { button.disabled = true if (fieldName == "full_name"){ - handleFullNameField(fieldName); + handleFullNameField(); }else { showInputFieldHideReadonlyField(fieldName, button); } + // Hide the button itself button.classList.add("display-none"); // Unlock after it completes From 82394c6d044d962b78ade43d500eba6ac6d84e72 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 14:16:09 -0600 Subject: [PATCH 58/66] Fix unit tests --- .../includes/finish_profile_form.html | 8 ++++---- src/registrar/templatetags/field_helpers.py | 4 ---- src/registrar/tests/test_views.py | 19 ++++++++++--------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 5fca63ce3..86e2e386a 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -33,7 +33,7 @@ Your contact information - {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable--no-border padding-top-2" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} @@ -52,7 +52,7 @@
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %} - {% with show_readonly=True add_class="display-none" group_classes="usa-form-editable padding-top-2 bold-usa-label" %} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %} {% with link_href=login_help_url %} {% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %} {% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %} @@ -62,11 +62,11 @@ {% endwith %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="padding-top-2" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="padding-top-2" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index be78099db..b72f77e21 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -97,10 +97,6 @@ def input_with_errors(context, field=None): # noqa: C901 # Used such that we can toggle it with JS if "display-none" not in classes: classes.append("display-none") - - # Tag that this form contains the edit button. - if "usa-form-editable" not in group_classes: - group_classes.append("usa-form-editable") attrs["id"] = field.auto_id diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index bfe4fd142..4c0ba08ee 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -628,9 +628,12 @@ class FinishUserProfileTests(TestWithUser, WebTest): completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True) self.assertEqual(completed_setup_page.status_code, 200) + # Assert that we're on the domain request page - self.assertContains(completed_setup_page, "How we’ll reach you") - self.assertContains(completed_setup_page, "Your contact information") + self.assertNotContains(completed_setup_page, "Finish setting up your profile") + self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?") + + self.assertContains(completed_setup_page, "You’re about to start your .gov domain request") @less_console_noise_decorator def test_new_user_with_profile_feature_off(self): @@ -645,8 +648,11 @@ class FinishUserProfileTests(TestWithUser, WebTest): when profile_feature is off but not the setup page""" with override_flag("profile_feature", active=False): response = self.client.get("/request/") - self.assertContains(response, "How we’ll reach you") - self.assertContains(response, "Your contact information") + + self.assertNotContains(response, "Finish setting up your profile") + self.assertNotContains(response, "What contact information should we use to reach you?") + + self.assertContains(response, "You’re about to start your .gov domain request") class UserProfileTests(TestWithUser, WebTest): @@ -806,11 +812,6 @@ class UserProfileTests(TestWithUser, WebTest): profile_page = self.app.get(reverse("user-profile")) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - profile_form = profile_page.form - profile_page = profile_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - profile_form = profile_page.form profile_form["title"] = "sample title" profile_form["phone"] = "(201) 555-1212" From efbcc5fcde122f42e57e9c1624cd686bb3e6eeb7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:59:52 -0600 Subject: [PATCH 59/66] Change link --- src/registrar/templates/includes/finish_profile_form.html | 2 +- src/registrar/templates/includes/profile_form.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 86e2e386a..1e928a27f 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -9,7 +9,7 @@ {% block profile_blurb %}

- We require + We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index f0ec0bc89..018291092 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -6,7 +6,7 @@ {% endblock profile_header %} {% block profile_blurb %} -

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

+

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

Contact information

Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

From 259aa7ccc91732ba61419fa3ca61548ff59931ed Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:56:40 -0600 Subject: [PATCH 60/66] PR suggestions --- src/registrar/assets/sass/_theme/_buttons.scss | 7 ++++++- src/registrar/templates/includes/finish_profile_form.html | 2 +- src/registrar/templates/includes/profile_form.html | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 1f5047503..9252fd5cf 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -7,11 +7,16 @@ a[href$="todo"]::after { content: " [link TBD]"; font-style: italic; } - + +a.usa-link { + color: #{$dhs-blue}; +} + a.breadcrumb__back { display:flex; align-items: center; margin-bottom: units(2.5); + color: #{$dhs-blue}; &:visited { color: color('primary'); } diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 1e928a27f..f5962eb9d 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -68,7 +68,7 @@ {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone %} + {% input_with_errors form.phone.as_national %} {% endwith %} {% endwith %} diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index 018291092..cd3e1d008 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -42,7 +42,7 @@ {% endwith %} {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone %} + {% input_with_errors form.phone.as_national %} {% endwith %} From 3f9ffd332d9460dc10cdc1769209b290f6f3d804 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 11:18:42 -0600 Subject: [PATCH 61/66] Remove as national --- src/registrar/templates/includes/finish_profile_form.html | 2 +- src/registrar/templates/includes/profile_form.html | 2 +- src/registrar/views/user_profile.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index f5962eb9d..1e928a27f 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -68,7 +68,7 @@ {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone.as_national %} + {% input_with_errors form.phone %} {% endwith %} {% endwith %} diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index cd3e1d008..018291092 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -42,7 +42,7 @@ {% endwith %} {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone.as_national %} + {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index a4756f482..e7e50baec 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -4,6 +4,7 @@ from enum import Enum import logging + from urllib.parse import quote from django.contrib import messages From 273f88ab145643f9e2d7c54e1aecd4ca07fe652d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:03:25 -0600 Subject: [PATCH 62/66] Format phone on readonly --- src/registrar/assets/sass/_theme/_buttons.scss | 1 + src/registrar/forms/user_profile.py | 4 ++++ src/registrar/templates/includes/readonly_input.html | 6 +++++- src/registrar/templatetags/custom_filters.py | 12 ++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 9252fd5cf..6b08f9b35 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -1,4 +1,5 @@ @use "uswds-core" as *; +@use "cisa_colors" as *; /* Make "placeholder" links visually obvious */ a[href$="todo"]::after { diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 55e72fc16..f12ed0f5f 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -60,6 +60,10 @@ class UserProfileForm(forms.ModelForm): } self.fields["phone"].error_messages["required"] = "Enter your phone number." + if self.instance and self.instance.phone: + print(f"what is the instace? {self.instance.phone}") + self.fields["phone"].initial = self.instance.phone.as_national + DomainHelper.disable_field(self.fields["email"], disable_required=True) diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html index 59a55090c..ebd5d788e 100644 --- a/src/registrar/templates/includes/readonly_input.html +++ b/src/registrar/templates/includes/readonly_input.html @@ -1,4 +1,4 @@ -{% load static field_helpers url_helpers %} +{% load static field_helpers url_helpers custom_filters %}
+ {% if field.name != "phone" %} {{ field.value }} + {% else %} + {{ field.value|format_phone }} + {% endif %}
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index 798558355..b5bb37b2c 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -2,6 +2,8 @@ import logging from django import template import re from registrar.models.domain_request import DomainRequest +from phonenumber_field.phonenumber import PhoneNumber +from phonenumber_field.widgets import RegionalPhoneNumberWidget register = template.Library() logger = logging.getLogger(__name__) @@ -133,3 +135,13 @@ def get_region(state): return regions.get(state.upper(), "N/A") else: return None + +@register.filter +def format_phone(value): + """Converts a phonenumber to a national format""" + if value: + phone_number = value + if isinstance(value, str): + phone_number = PhoneNumber.from_string(value) + return phone_number.as_national + return value From d9cdc2138997a1462212519194831acbe032d56a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:48:33 -0600 Subject: [PATCH 63/66] Update user_profile.py --- src/registrar/forms/user_profile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index f12ed0f5f..557e34e0d 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -61,7 +61,6 @@ class UserProfileForm(forms.ModelForm): self.fields["phone"].error_messages["required"] = "Enter your phone number." if self.instance and self.instance.phone: - print(f"what is the instace? {self.instance.phone}") self.fields["phone"].initial = self.instance.phone.as_national DomainHelper.disable_field(self.fields["email"], disable_required=True) From b3166d9de1f17f12363b31781fd9fd8ae4377aac Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:58:56 -0600 Subject: [PATCH 64/66] Merge conflict --- src/registrar/templates/includes/profile_form.html | 2 +- src/registrar/templates/profile.html | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index 018291092..d229801f3 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -6,7 +6,7 @@ {% endblock profile_header %} {% block profile_blurb %} -

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

+

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

Contact information

Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 12441da66..c126204bd 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -20,16 +20,22 @@ Edit your User Profile | {% include "includes/form_errors.html" with form=form %} {% if show_back_button %} - + - + {% if not return_to_request %}

{{ profile_back_button_text }}

+ {% else %} +

+ Go back to your domain request +

+ {% endif %}
{% endif %} + {% endblock content %} {% block content_bottom %} From d9cb7ab53fbf48d98286bfd1d63733b04bc239e8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:18:38 -0600 Subject: [PATCH 65/66] Linting --- src/registrar/models/utility/generic_helper.py | 1 - src/registrar/templatetags/custom_filters.py | 2 +- src/registrar/views/user_profile.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 8319416df..ca6ce6c31 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,7 +2,6 @@ import time import logging -from typing import Any from urllib.parse import urlparse, urlunparse, urlencode diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index b5bb37b2c..62afa1acb 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -3,7 +3,6 @@ from django import template import re from registrar.models.domain_request import DomainRequest from phonenumber_field.phonenumber import PhoneNumber -from phonenumber_field.widgets import RegionalPhoneNumberWidget register = template.Library() logger = logging.getLogger(__name__) @@ -136,6 +135,7 @@ def get_region(state): else: return None + @register.filter def format_phone(value): """Converts a phonenumber to a national format""" diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 41a462f49..47b01f7cb 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -19,7 +19,6 @@ from registrar.models.utility.generic_helper import replace_url_queryparams from registrar.views.utility.permission_views import UserProfilePermissionView from waffle.decorators import flag_is_active, waffle_flag -from registrar.models.utility.generic_helper import replace_url_queryparams from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_protect From 20f7fc8baf8d60f2989ed46173a2be303ba7fe10 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 3 Jun 2024 13:33:26 -0600 Subject: [PATCH 66/66] Make selector more specific --- src/registrar/assets/sass/_theme/_buttons.scss | 2 +- src/registrar/templates/includes/finish_profile_form.html | 2 +- src/registrar/templates/includes/profile_form.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 6b08f9b35..4024a6f53 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -9,7 +9,7 @@ a[href$="todo"]::after { font-style: italic; } -a.usa-link { +a.usa-link.usa-link--always-blue { color: #{$dhs-blue}; } diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html index 1e928a27f..a40534b48 100644 --- a/src/registrar/templates/includes/finish_profile_form.html +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -9,7 +9,7 @@ {% block profile_blurb %}

- We require + We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index d229801f3..cb3e734bf 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -6,7 +6,7 @@ {% endblock profile_header %} {% block profile_blurb %} -

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

+

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

Contact information

Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.