diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index c33a31aa2..4a6d98522 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1248,3 +1248,125 @@ document.addEventListener('DOMContentLoaded', function() { loadDomainRequests(1); } }); + + + +/** + * An IIFE that hooks up the edit buttons on the finish-user-setup page + */ +(function finishUserSetupListener() { + + function getInputField(fieldName){ + return document.querySelector(`#id_${fieldName}`) + } + + // Shows the hidden input field and hides the readonly one + function showInputFieldHideReadonlyField(fieldName, button) { + let inputField = getInputField(fieldName) + let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) + + readonlyField.classList.toggle('display-none'); + inputField.classList.toggle('display-none'); + + // Toggle the bold style on the grid row + let gridRow = button.closest(".grid-col-2").closest(".grid-row") + if (gridRow){ + gridRow.classList.toggle("bold-usa-label") + } + } + + function handleFullNameField(fieldName = "full_name") { + // Remove the display-none class from the nearest parent div + 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) { + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.classList.add("display-none"); + } + } + } + + function handleEditButtonClick(fieldName, button){ + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true + + if (fieldName == "full_name"){ + handleFullNameField(); + }else { + showInputFieldHideReadonlyField(fieldName, button); + } + + // Hide the button itself + 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() { + // Get all input elements within the form + let form = document.querySelector("#finish-profile-setup-form"); + let inputs = form ? form.querySelectorAll("input") : null; + if (!inputs) { + return null; + } + + let fullNameButtonClicked = false + 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; + } + + let editButton = document.querySelector(`#${fieldName}__edit-button`); + if (editButton){ + // Show the input field of the field that errored out + editButton.click(); + } + + // If either the full_name field errors out, + // or if any of its associated fields do - show all name related fields. + let nameFields = ["first_name", "middle_name", "last_name"]; + if (nameFields.includes(fieldName) && !fullNameButtonClicked){ + // Click the full name button if any of its related fields error out + fullNameButton = document.querySelector("#full_name__edit-button"); + if (fullNameButton) { + fullNameButton.click(); + fullNameButtonClicked = true; + } + } + }); + }); + }; + + // Hookup all edit buttons to the `handleEditButtonClick` function + setupListener(); + + // Show the input fields if an error exists + showInputOnErrorFields(); +})(); diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index dc115d69e..e88d75f4e 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 { @@ -169,3 +170,44 @@ abbr[title] { .cursor-pointer { cursor: pointer; } + +.input-with-edit-button { + svg.usa-icon { + width: 1.5em !important; + height: 1.5em !important; + color: #{$dhs-green}; + position: absolute; + } + &.input-with-edit-button__error { + svg.usa-icon { + color: #{$dhs-red}; + } + div.readonly-field { + color: #{$dhs-red}; + } + } +} + +// We need to deviate from some default USWDS styles here +// in this particular case, so we have to override this. +.usa-form .usa-button.readonly-edit-button { + margin-top: 0px !important; + padding-top: 0px !important; + svg { + width: 1.25em !important; + 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/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 1f5047503..4024a6f53 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 { @@ -7,11 +8,16 @@ a[href$="todo"]::after { content: " [link TBD]"; font-style: italic; } - + +a.usa-link.usa-link--always-blue { + 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/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/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 058a9f6c8..b5229fae1 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); @@ -26,6 +27,34 @@ } } +.usa-form-editable { + border-top: 2px #{$dhs-dark-gray-15} solid; + + .bold-usa-label label.usa-label{ + font-weight: bold; + } + + &.bold-usa-label label.usa-label{ + font-weight: bold; + } + + &.usa-form-editable--no-border { + border-top: None; + margin-top: 0px !important; + } + +} + +.usa-form-editable > .usa-form-group:first-of-type { + margin-top: unset; +} + +@media (min-width: 35em) { + .usa-form--largest { + max-width: 35rem; + } +} + .usa-form-group--unstyled-error { margin-left: 0; padding-left: 0; diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/sass/_theme/_tooltips.scss index 04c6f3cda..3ab630dc0 100644 --- a/src/registrar/assets/sass/_theme/_tooltips.scss +++ b/src/registrar/assets/sass/_theme/_tooltips.scss @@ -24,3 +24,7 @@ text-align: center !important; } } + +#extended-logo .usa-tooltip__body { + font-weight: 400 !important; +} diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 3f3af135a..851f3550c 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -162,7 +162,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 @@ -188,6 +188,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/config/urls.py b/src/registrar/config/urls.py index 1279e0fd8..bf13b950e 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -180,6 +180,11 @@ urlpatterns = [ views.DomainAddUserView.as_view(), name="domain-users-add", ), + path( + "finish-profile-setup", + views.FinishProfileSetupView.as_view(), + name="finish-user-profile-setup", + ), path( "user-profile", views.UserProfileView.as_view(), diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 11b5cc069..557e34e0d 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -60,4 +60,35 @@ class UserProfileForm(forms.ModelForm): } self.fields["phone"].error_messages["required"] = "Enter your phone number." + if self.instance and self.instance.phone: + self.fields["phone"].initial = self.instance.phone.as_national + 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 + self.fields["full_name"].initial = self.instance.get_formatted_name() diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index ce14c0a69..005037925 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -98,6 +98,24 @@ class User(AbstractUser): help_text="The means through which this user was verified", ) + @property + def finished_setup(self): + """ + Tracks if the user finished their profile setup or not. This is so + we can globally enforce that new users provide additional account information before proceeding. + """ + + # Change this to self once the user and contact objects are merged. + # For now, since they are linked, lets test on the underlying contact object. + user_info = self.contact # noqa + user_values = [ + user_info.first_name, + user_info.last_name, + user_info.title, + user_info.phone, + ] + return None not in user_values + def __str__(self): # this info is pulled from Login.gov if self.first_name or self.last_name: 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..f9921513b --- /dev/null +++ b/src/registrar/registrar_middleware.py @@ -0,0 +1,100 @@ +""" +Contains middleware used in settings.py +""" + +from urllib.parse import parse_qs +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 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 + + +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 + + self.setup_page = reverse("finish-user-profile-setup") + self.logout_page = reverse("logout") + self.excluded_pages = [ + self.setup_page, + self.logout_page, + ] + + def __call__(self, request): + response = self.get_response(request) + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + """Runs pre-processing logic for each view. Checks for the + finished_setup flag on the current user. If they haven't done so, + then we redirect them to the finish setup page.""" + # 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 check entirely + if not has_profile_feature_flag: + return None + + if request.user.is_authenticated: + if hasattr(request.user, "finished_setup") and not request.user.finished_setup: + return self._handle_setup_not_finished(request) + + # Continue processing the view + return None + + def _handle_setup_not_finished(self, request): + """Redirects the given user to the finish setup page. + + We set the "redirect" query param equal to where the user wants to go. + + If the user wants to go to '/request/', then we set that + information in the query param. + + Otherwise, we assume they want to go to the home 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 self.excluded_pages): + + # Preserve the original query parameters, and coerce them into a dict + query_params = parse_qs(request.META["QUERY_STRING"]) + + # Set the redirect value to our redirect location + if custom_redirect is not None: + query_params["redirect"] = custom_redirect + + # Add our new query param, while preserving old ones + new_setup_page = replace_url_queryparams(self.setup_page, query_params) if query_params else self.setup_page + + return HttpResponseRedirect(new_setup_page) + else: + # Process the view as normal + return None diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 958813029..fc49c19ec 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -139,11 +139,7 @@
+ 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. +
+ ++ Review the details below and update any required information. + 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 this is more useful as a default #} +{% include "includes/required_fields.html" with remove_margin_top=True %} + +{% endblock profile_blurb %} + +{% block profile_form %} + + +{% endblock profile_form %} diff --git a/src/registrar/templates/includes/footer.html b/src/registrar/templates/includes/footer.html index 42bd4ddcf..fdf91a64e 100644 --- a/src/registrar/templates/includes/footer.html +++ b/src/registrar/templates/includes/footer.html @@ -26,10 +26,12 @@ >