Pushing old commit content to save work already done
This commit is contained in:
zandercymatics 2024-05-15 15:24:19 -06:00
parent 04ced13eb6
commit bb42732c80
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
9 changed files with 211 additions and 235 deletions

View file

@ -104,8 +104,8 @@ urlpatterns = [
# 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-user-setup/<int:pk>",
views.ContactProfileSetupView.as_view(), views.FinishUserSetupView.as_view(),
name="finish-contact-profile-setup", name="finish-user-profile-setup",
), ),
path( path(
"domain-request/<id>/edit/", "domain-request/<id>/edit/",

View file

@ -203,7 +203,6 @@ NameserverFormset = formset_factory(
) )
# TODO - refactor, wait until daves PR
class ContactForm(forms.ModelForm): class ContactForm(forms.ModelForm):
"""Form for updating contacts.""" """Form for updating contacts."""

View file

@ -2,8 +2,8 @@ from django import forms
from phonenumber_field.formfields import PhoneNumberField # type: ignore from phonenumber_field.formfields import PhoneNumberField # type: ignore
class ContactForm(forms.Form): class FinishUserSetupForm(forms.Form):
"""Form for adding or editing a contact""" """Form for adding or editing user information"""
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()

View file

@ -22,7 +22,6 @@ class RegistrarForm(forms.Form):
kwargs.setdefault("label_suffix", "") kwargs.setdefault("label_suffix", "")
# save a reference to a domain request object # save a reference to a domain request object
self.domain_request = kwargs.pop("domain_request", None) self.domain_request = kwargs.pop("domain_request", None)
super(RegistrarForm, self).__init__(*args, **kwargs) super(RegistrarForm, self).__init__(*args, **kwargs)
def to_database(self, obj: DomainRequest | Contact): def to_database(self, obj: DomainRequest | Contact):

View file

@ -2,7 +2,8 @@
import time import time
import logging import logging
from typing import Any
from urllib.parse import urlparse, urlunparse, urlencode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -286,3 +287,27 @@ def from_database(form_class, obj):
if obj is None: if obj is None:
return {} return {}
return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore 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

View file

@ -7,6 +7,8 @@ from django.urls import reverse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from waffle.decorators import flag_is_active from waffle.decorators import flag_is_active
from registrar.models.utility.generic_helper import replace_url_queryparams
class CheckUserProfileMiddleware: class CheckUserProfileMiddleware:
""" """
@ -28,26 +30,29 @@ class CheckUserProfileMiddleware:
# Check that the user is "opted-in" to the profile feature flag # Check that the user is "opted-in" to the profile feature flag
has_profile_feature_flag = flag_is_active(request, "profile_feature") 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: if not has_profile_feature_flag:
return None return None
# Check if setup is not finished # Check if setup is not finished
finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup
if request.user.is_authenticated and not finished_setup: if request.user.is_authenticated and not finished_setup:
setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) 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") logout_page = reverse("logout")
excluded_pages = [ excluded_pages = [
setup_page, setup_page,
logout_page, logout_page,
] ]
custom_redirect = None
# In some cases, we don't want to redirect to home. # In some cases, we don't want to redirect to home. This handles that.
# This handles that. # Can easily be generalized if need be, but for now lets keep this easy to read.
if request.path == "/request/": custom_redirect = "domain-request:" if request.path == "/request/" else None
# 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) # Don't redirect on excluded pages (such as the setup page itself)
if not any(request.path.startswith(page) for page in excluded_pages): if not any(request.path.startswith(page) for page in excluded_pages):
@ -59,17 +64,12 @@ class CheckUserProfileMiddleware:
query_params["redirect"] = custom_redirect query_params["redirect"] = custom_redirect
if query_params: if query_params:
# Split the URL into parts setup_page = replace_url_queryparams(setup_page, query_params)
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 # Redirect to the setup page
return HttpResponseRedirect(setup_page) return HttpResponseRedirect(setup_page)
else:
# Continue processing the view # Process the view as normal
return None return None

View file

@ -14,8 +14,8 @@ from .domain import (
DomainInvitationDeleteView, DomainInvitationDeleteView,
DomainDeleteUserView, DomainDeleteUserView,
) )
from .contact import ( from .finish_user_setup import (
ContactProfileSetupView, FinishUserSetupView,
) )
from .health import * from .health import *
from .index import * from .index import *

View file

@ -1,13 +1,14 @@
from enum import Enum
from waffle.decorators import waffle_flag 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 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 django.contrib.messages.views import SuccessMessageMixin
from registrar.models.contact import Contact from registrar.models.contact import Contact
from registrar.templatetags.url_helpers import public_site_url from registrar.templatetags.url_helpers import public_site_url
from registrar.views.utility.permission_views import ContactPermissionView from registrar.views.utility.permission_views import ContactPermissionView
from django.views.generic.edit import FormMixin 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.safestring import mark_safe
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -19,27 +20,17 @@ logger = logging.getLogger(__name__)
class BaseContactView(SuccessMessageMixin, 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, cleaned_data): def get_success_message(self, cleaned_data):
"""Content of the returned success message""" """Content of the returned success message"""
return "Contact updated successfully." return "Contact updated successfully."
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""Sets the current contact in cache, defines the current object as self.object self._update_object_and_session(request)
then returns render_to_response"""
self._set_contact(request)
context = self.get_context_data(object=self.object) context = self.get_context_data(object=self.object)
return self.render_to_response(context) return self.render_to_response(context)
def _set_contact(self, request): def _update_object_and_session(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
"""
self.session = request.session self.session = request.session
contact_pk = "contact:" + str(self.kwargs.get("pk")) contact_pk = "contact:" + str(self.kwargs.get("pk"))
@ -50,9 +41,9 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView):
else: else:
self.object = self.get_object() 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 Set contact pk in the session cache
""" """
@ -63,46 +54,30 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView):
class ContactFormBaseView(BaseContactView, FormMixin): class ContactFormBaseView(BaseContactView, FormMixin):
"""Adds a FormMixin to BaseContactView, and handles post""" """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): def form_invalid(self, form):
# updates session cache with contact # updates session cache with contact
self._update_session_with_contact() self._refresh_session()
# superclass has the redirect # superclass has the redirect
return super().form_invalid(form) return super().form_invalid(form)
class ContactProfileSetupView(ContactFormBaseView): class FinishUserSetupView(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""" we may have missed from Login.gov"""
template_name = "finish_contact_setup.html" template_name = "finish_contact_setup.html"
form_class = ContactForm form_class = FinishUserSetupForm
model = Contact model = Contact
redirect_type = None redirect_type = None
# TODO - make this an enum class RedirectType(Enum):
class RedirectType:
""" """
Contains constants for each type of redirection. Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`.
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") - 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" - 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 - 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 redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE
@ -113,146 +88,6 @@ class ContactProfileSetupView(ContactFormBaseView):
COMPLETE_SETUP = "complete_setup" COMPLETE_SETUP = "complete_setup"
TO_SPECIFIC_PAGE = "domain_request" 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): def get_initial(self):
"""The initial value for the form (which is a formset here).""" """The initial value for the form (which is a formset here)."""
db_object = from_database(form_class=self.form_class, obj=self.object) 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, youll need to update your Login.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>' f'and log back in. <a class="usa-link" href={help_url}>Get help with your Login.gov account.</a>'
) # nosec ) # 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

View file

@ -358,18 +358,6 @@ class ContactPermission(PermissionsLoginMixin):
if not requested_user_exists or not requested_contact_exists: if not requested_user_exists or not requested_contact_exists:
return False 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