Replace form with modelform

Pull in content from related PR and refactor around it
This commit is contained in:
zandercymatics 2024-05-20 11:25:32 -06:00
parent d202d2601f
commit 9a6ccc1c44
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
10 changed files with 230 additions and 363 deletions

View file

@ -873,35 +873,16 @@ function hideDeletedForms() {
let fieldId = getInputFieldId(fieldName) let fieldId = getInputFieldId(fieldName)
let inputField = document.querySelector(fieldId); let inputField = document.querySelector(fieldId);
let nameFieldset = document.querySelector("#contact-full-name-fieldset"); let nameFieldset = document.querySelector("#profile-name-fieldset");
if (nameFieldset){ if (nameFieldset){
nameFieldset.classList.remove("display-none"); nameFieldset.classList.remove("display-none");
} }
if (inputField) { if (inputField) {
let readonlyId = getReadonlyFieldId(fieldName) // Remove the "full_name" field
let readonlyField = document.querySelector(readonlyId) inputFieldParentDiv = inputField.closest("div");
if (readonlyField) { if (inputFieldParentDiv) {
// Update the <use> element's xlink:href attribute inputFieldParentDiv.remove();
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");
}
} }
} }
} }

View file

@ -10,23 +10,8 @@ fieldset:not(:first-child) {
} }
fieldset.registrar-fieldset__contact { fieldset.registrar-fieldset__contact {
border-width: 2px; // This fieldset is for SR purposes only
border-left: none; border: 0;
border-right: none; margin: 0;
border-bottom: none; padding: 0;
padding-bottom: 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;
}
}

View file

@ -103,8 +103,8 @@ urlpatterns = [
path( path(
# We embed the current user ID here, but we have a permission check # We embed the current user ID here, but we have a permission check
# that ensures the user is who they say they are. # that ensures the user is who they say they are.
"finish-user-setup/<int:pk>", "finish-profile-setup/<int:pk>",
views.FinishUserSetupView.as_view(), views.FinishProfileSetupView.as_view(),
name="finish-user-profile-setup", name="finish-user-profile-setup",
), ),
path( path(

View file

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

View file

@ -55,6 +55,34 @@ class UserProfileForm(forms.ModelForm):
"required": "Enter your email address in the required format, like name@example.com." "required": "Enter your email address in the required format, like name@example.com."
} }
self.fields["phone"].error_messages["required"] = "Enter your phone number." self.fields["phone"].error_messages["required"] = "Enter your phone number."
self.domainInfo = None
DomainHelper.disable_field(self.fields["email"], disable_required=True) 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()

View file

@ -102,13 +102,6 @@ class Contact(TimeStampedModel):
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
return " ".join(names) if names else "Unknown" 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): def has_contact_info(self):
return bool(self.title or self.email or self.phone) return bool(self.title or self.email or self.phone)

View file

@ -62,25 +62,17 @@
<legend class="usa-sr-only"> <legend class="usa-sr-only">
Your contact information Your contact information
</legend> </legend>
{# 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" %} {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %}
{% input_with_errors form.full_name %} {% input_with_errors form.full_name %}
{% endwith %} {% endwith %}
<fieldset id="contact-full-name-fieldset" class="registrar-fieldset__contact display-none"> <fieldset id="profile-name-fieldset" class="registrar-fieldset__contact display-none">
{% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly" %} {% input_with_errors form.first_name %}
{% input_with_errors form.first_name %}
{% endwith %}
{% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.middle_name %}
{% input_with_errors form.middle_name %}
{% endwith %}
{% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2"%} {% input_with_errors form.last_name %}
{% input_with_errors form.last_name %}
{% endwith %}
</fieldset> </fieldset>
{# This field doesn't have the readonly button but it has common design elements from it #} {# This field doesn't have the readonly button but it has common design elements from it #}

View file

@ -14,9 +14,6 @@ from .domain import (
DomainInvitationDeleteView, DomainInvitationDeleteView,
DomainDeleteUserView, DomainDeleteUserView,
) )
from .user_profile import UserProfileView from .user_profile import UserProfileView, FinishProfileSetupView
from .finish_user_setup import (
FinishUserSetupView,
)
from .health import * from .health import *
from .index import * from .index import *

View file

@ -1,241 +0,0 @@
from enum import Enum
from waffle.decorators import waffle_flag
from urllib.parse import quote
from django.urls import NoReverseMatch, reverse
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 replace_url_queryparams, 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
import logging
logger = logging.getLogger(__name__)
class BaseContactView(SuccessMessageMixin, ContactPermissionView):
def get_success_message(self, cleaned_data):
"""Content of the returned success message"""
return "Contact updated successfully."
def get(self, request, *args, **kwargs):
self._update_object_and_session(request)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def _update_object_and_session(self, request):
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._refresh_session()
def _refresh_session(self):
"""
Set contact pk in the session cache
"""
contact_pk = "contact:" + str(self.kwargs.get("pk"))
self.session[contact_pk] = self.object
class ContactFormBaseView(BaseContactView, FormMixin):
"""Adds a FormMixin to BaseContactView, and handles post"""
def form_invalid(self, form):
# updates session cache with contact
self._refresh_session()
# superclass has the redirect
return super().form_invalid(form)
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 = FinishUserSetupForm
model = Contact
redirect_type = None
class RedirectType(Enum):
"""
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-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
"""
HOME = "home"
BACK_TO_SELF = "back_to_self"
COMPLETE_SETUP = "complete_setup"
TO_SPECIFIC_PAGE = "domain_request"
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()
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")
return mark_safe(
"We recommend using your work email for your .gov account. "
"If the wrong email is displayed below, youll need to update your Login.gov account "
f'and log back in. <a class="usa-link" href={help_url}>Get help with your Login.gov account.</a>'
) # 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", self.RedirectType.BACK_TO_SELF)
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():
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
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

View file

@ -2,18 +2,25 @@
""" """
from enum import Enum
import logging import logging
from urllib.parse import quote
from django.contrib import messages from django.contrib import messages
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from registrar.forms.user_profile import UserProfileForm from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
from django.urls import reverse from django.urls import NoReverseMatch, reverse
from registrar.models import ( from registrar.models import (
Contact, Contact,
) )
from registrar.views.utility.permission_views import UserProfilePermissionView from registrar.views.utility.permission_views import UserProfilePermissionView
from waffle.decorators import flag_is_active, waffle_flag from waffle.decorators import flag_is_active, waffle_flag
from registrar.templatetags.url_helpers import public_site_url
from registrar.models.utility.generic_helper import replace_url_queryparams
from django.utils.safestring import mark_safe
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,11 +37,16 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Handle get requests by getting user's contact object and setting object """Handle get requests by getting user's contact object and setting object
and form to context before rendering.""" and form to context before rendering."""
self.object = self.get_object() self._refresh_session_and_object(request)
form = self.form_class(instance=self.object) form = self.form_class(instance=self.object)
context = self.get_context_data(object=self.object, form=form) context = self.get_context_data(object=self.object, form=form)
return self.render_to_response(context) return self.render_to_response(context)
def _refresh_session_and_object(self, request):
"""Sets the current session to self.session and the current object to self.object"""
self.session = request.session
self.object = self.get_object()
@waffle_flag("profile_feature") # type: ignore @waffle_flag("profile_feature") # type: ignore
def dispatch(self, request, *args, **kwargs): # type: ignore def dispatch(self, request, *args, **kwargs): # type: ignore
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -52,7 +64,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Handle post requests (form submissions)""" """Handle post requests (form submissions)"""
self.object = self.get_object() self._refresh_session_and_object(request)
form = self.form_class(request.POST, instance=self.object) form = self.form_class(request.POST, instance=self.object)
if form.is_valid(): if form.is_valid():
@ -75,3 +87,171 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
if hasattr(user, "contact"): # Check if the user has a contact instance if hasattr(user, "contact"): # Check if the user has a contact instance
return user.contact return user.contact
return None return None
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
redirect_type = None
class RedirectType(Enum):
"""
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-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
"""
HOME = "home"
BACK_TO_SELF = "back_to_self"
COMPLETE_SETUP = "complete_setup"
TO_SPECIFIC_PAGE = "domain_request"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["email_sublabel_text"] = self._email_sublabel_text()
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")
return mark_safe(
"We recommend using your work email for your .gov account. "
"If the wrong email is displayed below, youll need to update your Login.gov account "
f'and log back in. <a class="usa-link" href={help_url}>Get help with your Login.gov account.</a>'
) # nosec
def get_success_message(self, cleaned_data):
"""Content of the returned success message"""
return "Your profile has been successfully updated."
@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", self.RedirectType.BACK_TO_SELF)
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
"""
self._refresh_session_and_object(request)
form = self.form_class(request.POST, instance=self.object)
# 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
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()
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