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
# that ensures the user is who they say they are.
"finish-user-setup/<int:pk>",
views.ContactProfileSetupView.as_view(),
name="finish-contact-profile-setup",
views.FinishUserSetupView.as_view(),
name="finish-user-profile-setup",
),
path(
"domain-request/<id>/edit/",

View file

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

View file

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

View file

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

View file

@ -2,7 +2,8 @@
import time
import logging
from typing import Any
from urllib.parse import urlparse, urlunparse, urlencode
logger = logging.getLogger(__name__)
@ -286,3 +287,27 @@ def from_database(form_class, obj):
if obj is None:
return {}
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 waffle.decorators import flag_is_active
from registrar.models.utility.generic_helper import replace_url_queryparams
class CheckUserProfileMiddleware:
"""
@ -28,49 +30,47 @@ class CheckUserProfileMiddleware:
# 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 entirely
# If they aren't, skip this check entirely
if not has_profile_feature_flag:
return None
# Check if setup is not finished
finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup
if request.user.is_authenticated and not finished_setup:
setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk})
logout_page = reverse("logout")
excluded_pages = [
setup_page,
logout_page,
]
custom_redirect = None
# In some cases, we don't want to redirect to home.
# This handles that.
if request.path == "/request/":
# 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)
if not any(request.path.startswith(page) for page in excluded_pages):
# Preserve the original query parameters, and coerce them into a dict
query_params = parse_qs(request.META["QUERY_STRING"])
if custom_redirect is not None:
# Set the redirect value to our redirect location
query_params["redirect"] = custom_redirect
if query_params:
# Split the URL into parts
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
return HttpResponseRedirect(setup_page)
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")
excluded_pages = [
setup_page,
logout_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 excluded_pages):
# Preserve the original query parameters, and coerce them into a dict
query_params = parse_qs(request.META["QUERY_STRING"])
if custom_redirect is not None:
# Set the redirect value to our redirect location
query_params["redirect"] = custom_redirect
if query_params:
setup_page = replace_url_queryparams(setup_page, query_params)
# Redirect to the setup page
return HttpResponseRedirect(setup_page)
else:
# Process the view as normal
return None
class NoCacheMiddleware:

View file

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

View file

@ -1,13 +1,14 @@
from enum import Enum
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 registrar.forms.contact import ContactForm
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 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.decorators import method_decorator
@ -19,27 +20,17 @@ logger = logging.getLogger(__name__)
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):
"""Content of the returned success message"""
return "Contact updated successfully."
def get(self, request, *args, **kwargs):
"""Sets the current contact in cache, defines the current object as self.object
then returns render_to_response"""
self._set_contact(request)
self._update_object_and_session(request)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def _set_contact(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
"""
def _update_object_and_session(self, request):
self.session = request.session
contact_pk = "contact:" + str(self.kwargs.get("pk"))
@ -50,9 +41,9 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView):
else:
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
"""
@ -63,46 +54,30 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView):
class ContactFormBaseView(BaseContactView, FormMixin):
"""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):
# updates session cache with contact
self._update_session_with_contact()
self._refresh_session()
# superclass has the redirect
return super().form_invalid(form)
class ContactProfileSetupView(ContactFormBaseView):
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 = ContactForm
form_class = FinishUserSetupForm
model = Contact
redirect_type = None
# TODO - make this an enum
class RedirectType:
class RedirectType(Enum):
"""
Contains constants for each type of redirection.
Not an enum as we just need to track string values,
but we don't care about enforcing it.
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-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"
- 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
@ -113,146 +88,6 @@ class ContactProfileSetupView(ContactFormBaseView):
COMPLETE_SETUP = "complete_setup"
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):
"""The initial value for the form (which is a formset here)."""
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 "
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", 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,19 +358,7 @@ class ContactPermission(PermissionsLoginMixin):
if not requested_user_exists or not requested_contact_exists:
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
class DomainRequestPermissionWithdraw(PermissionsLoginMixin):