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 @@
{% block logo %} - + {% include "includes/gov_extended_logo.html" with logo_clickable=True %} {% endblock %}
@@ -160,7 +156,8 @@ {% if has_profile_feature_flag %}
  • {% url 'user-profile' as user_profile_url %} -
  • @@ -206,7 +203,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_profile_setup.html b/src/registrar/templates/finish_profile_setup.html new file mode 100644 index 000000000..f8070551b --- /dev/null +++ b/src/registrar/templates/finish_profile_setup.html @@ -0,0 +1,20 @@ +{% extends "profile.html" %} + +{% load static form_helpers url_helpers field_helpers %} +{% block title %} Finish setting up your profile | {% endblock %} + +{# Disable the redirect #} +{% block logo %} + {% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %} +{% endblock %} + +{# Add the new form #} +{% block content_bottom %} + {% include "includes/finish_profile_form.html" with form=form %} + + +{% endblock content_bottom %} + +{% block footer %} + {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %} +{% endblock footer %} diff --git a/src/registrar/templates/includes/finish_profile_form.html b/src/registrar/templates/includes/finish_profile_form.html new file mode 100644 index 000000000..a40534b48 --- /dev/null +++ b/src/registrar/templates/includes/finish_profile_form.html @@ -0,0 +1,89 @@ +{% extends 'includes/profile_form.html' %} + +{% load static url_helpers %} +{% load field_helpers %} + +{% block profile_header %} +

    Finish setting up your profile

    +{% 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. +

    + +

    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. +

    + +{# 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 %} +
    + {% csrf_token %} +
    + + Your contact information + + + {% 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 %} + + + + {% 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 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 %} + {% input_with_errors form.email %} + {% endwith %} + {% endwith %} + {% endwith %} + {% endwith %} + + {% 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="usa-form-editable padding-top-2" %} + {% with add_class="usa-input--medium" %} + {% input_with_errors form.phone %} + {% endwith %} + {% endwith %} + +
    +
    + + + {% if confirm_changes and going_to_specific_page %} + + {% endif %} +
    +
    + +{% 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 @@ >