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 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 <use> 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();
}
}
}

View file

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

View file

@ -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/<int:pk>",
views.FinishUserSetupView.as_view(),
"finish-profile-setup/<int:pk>",
views.FinishProfileSetupView.as_view(),
name="finish-user-profile-setup",
),
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."
}
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()

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]
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)

View file

@ -62,25 +62,17 @@
<legend class="usa-sr-only">
Your contact information
</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" %}
{% input_with_errors form.full_name %}
{% endwith %}
<fieldset id="contact-full-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 %}
{% endwith %}
<fieldset id="profile-name-fieldset" class="registrar-fieldset__contact display-none">
{% input_with_errors form.first_name %}
{% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %}
{% input_with_errors form.middle_name %}
{% endwith %}
{% input_with_errors form.middle_name %}
{% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2"%}
{% input_with_errors form.last_name %}
{% endwith %}
{% input_with_errors form.last_name %}
</fieldset>
{# 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,
DomainDeleteUserView,
)
from .user_profile import UserProfileView
from .finish_user_setup import (
FinishUserSetupView,
)
from .user_profile import UserProfileView, FinishProfileSetupView
from .health 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
from urllib.parse import quote
from django.contrib import messages
from django.views.generic.edit import FormMixin
from registrar.forms.user_profile import UserProfileForm
from django.urls import reverse
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
from django.urls import NoReverseMatch, reverse
from registrar.models import (
Contact,
)
from registrar.views.utility.permission_views import UserProfilePermissionView
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__)
@ -30,11 +37,16 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
def get(self, request, *args, **kwargs):
"""Handle get requests by getting user's contact object and setting object
and form to context before rendering."""
self.object = self.get_object()
self._refresh_session_and_object(request)
form = self.form_class(instance=self.object)
context = self.get_context_data(object=self.object, form=form)
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
def dispatch(self, request, *args, **kwargs): # type: ignore
return super().dispatch(request, *args, **kwargs)
@ -52,7 +64,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
def post(self, request, *args, **kwargs):
"""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)
if form.is_valid():
@ -75,3 +87,171 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
if hasattr(user, "contact"): # Check if the user has a contact instance
return user.contact
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