From 24ec18c439f2cb0d66b84fcbfbcd1736d68c07af Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 8 May 2024 15:13:03 -0600 Subject: [PATCH 01/35] Added logic to prevent creation of DomainInvitation if invite fails to send --- src/registrar/views/domain.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9134080a1..2d67fc477 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -776,15 +776,23 @@ class DomainAddUserView(DomainFormBaseView): def _make_invitation(self, email_address: str, requestor: User): """Make a Domain invitation for this email and redirect with a message.""" - invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) - if not created: + # Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.) + invite_exists = DomainInvitation.objects.get(email=email_address, domain=self.object) + if invite_exists: # that invitation already existed messages.warning( self.request, f"{email_address} has already been invited to this domain.", ) else: - self._send_domain_invitation_email(email=email_address, requestor=requestor) + #Try to send the invitation. If it succeeds, add it to the DomainInvitation table. + try: + self._send_domain_invitation_email(email=email_address, requestor=requestor) + except Exception as exc: + raise EmailSendingError("Could not send SES email.") from exc + else: + #(NOTE: if the invitation fails to send, no invitation should be added to the DomainInvitation table) + DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) return redirect(self.get_success_url()) def form_valid(self, form): From 5b8cf13eb183aeebbf474d91fe1458edc00446f5 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 8 May 2024 16:04:16 -0600 Subject: [PATCH 02/35] Fixed exception chain --- src/registrar/views/domain.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 2d67fc477..eb6f7d2c5 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -762,14 +762,14 @@ class DomainAddUserView(DomainFormBaseView): "requestor_email": requestor_email, }, ) - except EmailSendingError: - messages.warning(self.request, "Could not send email invitation.") + except EmailSendingError as exc: logger.warn( "Could not sent email invitation to %s for domain %s", email, self.object, exc_info=True, ) + raise EmailSendingError("Could not send email invitation.") from exc else: if add_success: messages.success(self.request, f"{email} has been invited to this domain.") @@ -777,21 +777,21 @@ class DomainAddUserView(DomainFormBaseView): def _make_invitation(self, email_address: str, requestor: User): """Make a Domain invitation for this email and redirect with a message.""" # Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.) - invite_exists = DomainInvitation.objects.get(email=email_address, domain=self.object) - if invite_exists: + try: + invite = DomainInvitation.objects.get(email=email_address, domain=self.object) # that invitation already existed messages.warning( self.request, f"{email_address} has already been invited to this domain.", ) - else: + except DomainInvitation.DoesNotExist: #Try to send the invitation. If it succeeds, add it to the DomainInvitation table. try: self._send_domain_invitation_email(email=email_address, requestor=requestor) - except Exception as exc: - raise EmailSendingError("Could not send SES email.") from exc + except EmailSendingError as exc: + messages.warning(self.request, "Could not send email invitation.") else: - #(NOTE: if the invitation fails to send, no invitation should be added to the DomainInvitation table) + #(NOTE: only create a domainInvitation if the e-mail sends correctly) DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) return redirect(self.get_success_url()) @@ -807,7 +807,10 @@ class DomainAddUserView(DomainFormBaseView): return self._make_invitation(requested_email, requestor) else: # if user already exists then just send an email - self._send_domain_invitation_email(requested_email, requestor, add_success=False) + try: + self._send_domain_invitation_email(requested_email, requestor, add_success=False) + except Exception as exc: + messages.warning(self.request, "Could not send email invitation.") try: UserDomainRole.objects.create( From 948afab1662a81cac1afbc26728529da521ac1fe Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 9 May 2024 11:14:49 -0400 Subject: [PATCH 03/35] wip --- src/registrar/config/urls.py | 5 +++ src/registrar/templates/base.html | 5 ++- src/registrar/templates/profile.html | 41 ++++++++++++------- src/registrar/views/__init__.py | 1 + src/registrar/views/user_profile.py | 36 ++++++++++++++++ src/registrar/views/utility/mixins.py | 15 +++++++ .../views/utility/permission_views.py | 23 ++++++++++- 7 files changed, 109 insertions(+), 17 deletions(-) create mode 100644 src/registrar/views/user_profile.py diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 720034150..380cdb803 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -178,6 +178,11 @@ urlpatterns = [ views.DomainAddUserView.as_view(), name="domain-users-add", ), + path( + "user-profile", + views.UserProfileView.as_view(), + name="user-profile", + ), path( "invitation//delete", views.DomainInvitationDeleteView.as_view(http_method_names=["post"]), diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index c0702e78f..efaf46dfa 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -158,8 +158,11 @@
  • | - Sign out + Your profile
  • +
  • + | + Sign out {% else %} Sign in {% endif %} diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 6a051bfe4..26fa34163 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -3,25 +3,36 @@ {% block title %} Edit your User Profile | {% endblock title %} +{% load static url_helpers %} {% block content %}
    -
    -
    - Your profile -

    - Required fields are marked with an asterisk (*). +

    - - +
    +

    Your profile

    +

    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.

    +

    Contact information

    +

    Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

    + {% include "includes/required_fields.html" %} + +
    +
    + Your profile + {% for field in profile_form %} + + {{ field }} + {% endfor %} +
    + +
    {% endblock content %} diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index bd15196d4..4ed5bfa75 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -14,5 +14,6 @@ from .domain import ( DomainInvitationDeleteView, DomainDeleteUserView, ) +from .user_profile import UserProfileView from .health import * from .index import * diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py new file mode 100644 index 000000000..e24982965 --- /dev/null +++ b/src/registrar/views/user_profile.py @@ -0,0 +1,36 @@ +"""Views for a User Profile. + +""" + +import logging + +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.db import IntegrityError +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic.edit import FormMixin +from django.conf import settings + +from registrar.models import ( + User, +) +from registrar.views.utility.permission_views import UserProfilePermissionView + + +logger = logging.getLogger(__name__) + + +class UserProfileView(UserProfilePermissionView): + """ + Base View for the Domain. Handles getting and setting the domain + in session cache on GETs. Also provides methods for getting + and setting the domain in cache + """ + + template_name = "profile.html" + + # Override get_object to return the logged-in user + def get_object(self, queryset=None): + return self.request.user # Returns the logged-in user \ No newline at end of file diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index c7083ce48..2208b7570 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -382,3 +382,18 @@ class DomainInvitationPermission(PermissionsLoginMixin): return False return True + + +class UserProfilePermission(PermissionsLoginMixin): + """Permission mixin that redirects to user profile if user + has access, otherwise 403""" + + def has_permission(self): + """Check if this user has access. + + If the user is authenticated, they have access + """ + if not self.request.user.is_authenticated: + return False + + return True diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index f2752c3b5..6f59cc032 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -2,8 +2,9 @@ import abc # abstract base class +from django.contrib.auth import get_user_model from django.views.generic import DetailView, DeleteView, TemplateView -from registrar.models import Domain, DomainRequest, DomainInvitation +from registrar.models import Domain, DomainRequest, DomainInvitation, User from registrar.models.user_domain_role import UserDomainRole from .mixins import ( @@ -13,6 +14,7 @@ from .mixins import ( DomainInvitationPermission, DomainRequestWizardPermission, UserDeleteDomainRolePermission, + UserProfilePermission, ) import logging @@ -142,3 +144,22 @@ class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteV # variable name in template context for the model object context_object_name = "userdomainrole" + + +class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC): + """Abstract base view for user profile view that enforces permissions. + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = get_user_model() + # variable name in template context for the model object + context_object_name = "user" + + # Abstract property enforces NotImplementedError on an attribute. + @property + @abc.abstractmethod + def template_name(self): + raise NotImplementedError From aa5b574d01cb785e661f0c4f7e83b0f5f81586c8 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Thu, 9 May 2024 11:54:16 -0400 Subject: [PATCH 04/35] wip --- src/registrar/templates/profile.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 26fa34163..9b43bcec1 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -25,7 +25,6 @@ Edit your User Profile |
    - Your profile {% for field in profile_form %} {{ field }} From f9249f88cdd8f1f2b741f4a39f3d826621508e58 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 14 May 2024 14:19:08 -0400 Subject: [PATCH 05/35] wip but not working, adding in pk --- src/registrar/config/urls.py | 2 +- src/registrar/forms/user_profile.py | 54 +++++++++++++++++++ src/registrar/templates/base.html | 2 +- src/registrar/templates/profile.html | 26 +++++++-- src/registrar/views/user_profile.py | 42 ++++++++++----- .../views/utility/permission_views.py | 11 ++-- 6 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 src/registrar/forms/user_profile.py diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 380cdb803..08383b797 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -179,7 +179,7 @@ urlpatterns = [ name="domain-users-add", ), path( - "user-profile", + "user-profile/", views.UserProfileView.as_view(), name="user-profile", ), diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py new file mode 100644 index 000000000..f436b5fe4 --- /dev/null +++ b/src/registrar/forms/user_profile.py @@ -0,0 +1,54 @@ +from django import forms + +from registrar.models.contact import Contact + +from django.core.validators import MaxLengthValidator +from phonenumber_field.widgets import RegionalPhoneNumberWidget + +class UserProfileForm(forms.ModelForm): + """Form for updating user profile.""" + + class Meta: + model = Contact + fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"] + widgets = { + "first_name": forms.TextInput, + "middle_name": forms.TextInput, + "last_name": forms.TextInput, + "title": forms.TextInput, + "email": forms.EmailInput, + "phone": RegionalPhoneNumberWidget, + } + + # the database fields have blank=True so ModelForm doesn't create + # required fields by default. Use this list in __init__ to mark each + # of these fields as required + required = ["first_name", "last_name", "title", "email", "phone"] + + # def __init__(self, *args, **kwargs): + # super().__init__(*args, **kwargs) + # # take off maxlength attribute for the phone number field + # # which interferes with out input_with_errors template tag + # self.fields["phone"].widget.attrs.pop("maxlength", None) + + # # Define a custom validator for the email field with a custom error message + # email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.") + # self.fields["email"].validators.append(email_max_length_validator) + + # for field_name in self.required: + # self.fields[field_name].required = True + + # # Set custom form label + # self.fields["middle_name"].label = "Middle name (optional)" + + # # Set custom error messages + # self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."} + # self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."} + # self.fields["title"].error_messages = { + # "required": "Enter your title or role in your organization (e.g., Chief Information Officer)" + # } + # self.fields["email"].error_messages = { + # "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 \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index efaf46dfa..4756f5976 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -158,7 +158,7 @@
  • | - Your profile + Your profile
  • | diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 9b43bcec1..e91a68042 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -1,9 +1,11 @@ {% extends 'base.html' %} + {% block title %} Edit your User Profile | {% endblock title %} {% load static url_helpers %} +{% load field_helpers %} {% block content %}
    @@ -23,12 +25,26 @@ Edit your User Profile |

    Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

    {% include "includes/required_fields.html" %} - + + {% csrf_token %}
    - {% for field in profile_form %} - - {{ field }} - {% endfor %} + + {% input_with_errors form.first_name %} + + {% input_with_errors form.middle_name %} + + {% input_with_errors form.last_name %} + + {% input_with_errors form.title %} + + {% input_with_errors form.email %} + + {% with add_class="usa-input--medium" %} + {% input_with_errors form.phone %} + {% endwith %} + + +
    diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index e24982965..b6ec2289d 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -4,17 +4,10 @@ import logging -from django.contrib import messages -from django.contrib.messages.views import SuccessMessageMixin -from django.db import IntegrityError -from django.http import HttpResponseRedirect -from django.shortcuts import redirect -from django.urls import reverse -from django.views.generic.edit import FormMixin -from django.conf import settings - +from registrar.forms.user_profile import UserProfileForm from registrar.models import ( User, + Contact, ) from registrar.views.utility.permission_views import UserProfilePermissionView @@ -29,8 +22,33 @@ class UserProfileView(UserProfilePermissionView): and setting the domain in cache """ + model = Contact template_name = "profile.html" + form_class = UserProfileForm + + # def get(self, request, *args, **kwargs): + # logger.info("in get") + # return super().get(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + logger.info("in get()") + self.object = self.get_object() + context = self.get_context_data(object=self.object) + logger.info(context) + return self.render_to_response(context) - # Override get_object to return the logged-in user - def get_object(self, queryset=None): - return self.request.user # Returns the logged-in user \ No newline at end of file + # def get_context_data(self, **kwargs): + # logger.info("in get_context_data") + # kwargs.setdefault("view", self) + # if self.extra_context is not None: + # kwargs.update(self.extra_context) + # return kwargs + + # # Override get_object to return the logged-in user + # def get_object(self, queryset=None): + # logger.info("in get_object") + # user = self.request.user # get the logged in user + # if hasattr(user, 'contact'): # Check if the user has a contact instance + # logger.info(user.contact) + # return user.contact + # return None \ No newline at end of file diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 6f59cc032..5ea9e1590 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -5,6 +5,7 @@ import abc # abstract base class from django.contrib.auth import get_user_model from django.views.generic import DetailView, DeleteView, TemplateView from registrar.models import Domain, DomainRequest, DomainInvitation, User +from registrar.models.contact import Contact from registrar.models.user_domain_role import UserDomainRole from .mixins import ( @@ -153,10 +154,12 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC): `template_name`. """ - # DetailView property for what model this is viewing - model = get_user_model() - # variable name in template context for the model object - context_object_name = "user" + # # DetailView property for what model this is viewing + # model = get_user_model() + # # variable name in template context for the model object + # context_object_name = "user" + model = Contact + context_object_name = "contact" # Abstract property enforces NotImplementedError on an attribute. @property From 5ed20c09c38271f93c7fb5d780afd7d90f45704f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Tue, 14 May 2024 15:34:45 -0400 Subject: [PATCH 06/35] forms now working --- src/registrar/config/urls.py | 2 +- src/registrar/forms/user_profile.py | 46 ++++++++++++++--------------- src/registrar/templates/base.html | 2 +- src/registrar/views/user_profile.py | 19 ++++++------ 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 08383b797..380cdb803 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -179,7 +179,7 @@ urlpatterns = [ name="domain-users-add", ), path( - "user-profile/", + "user-profile", views.UserProfileView.as_view(), name="user-profile", ), diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index f436b5fe4..fe4d6d609 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -25,30 +25,30 @@ class UserProfileForm(forms.ModelForm): # of these fields as required required = ["first_name", "last_name", "title", "email", "phone"] - # def __init__(self, *args, **kwargs): - # super().__init__(*args, **kwargs) - # # take off maxlength attribute for the phone number field - # # which interferes with out input_with_errors template tag - # self.fields["phone"].widget.attrs.pop("maxlength", None) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # take off maxlength attribute for the phone number field + # which interferes with out input_with_errors template tag + self.fields["phone"].widget.attrs.pop("maxlength", None) - # # Define a custom validator for the email field with a custom error message - # email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.") - # self.fields["email"].validators.append(email_max_length_validator) + # Define a custom validator for the email field with a custom error message + email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.") + self.fields["email"].validators.append(email_max_length_validator) - # for field_name in self.required: - # self.fields[field_name].required = True + for field_name in self.required: + self.fields[field_name].required = True - # # Set custom form label - # self.fields["middle_name"].label = "Middle name (optional)" + # Set custom form label + self.fields["middle_name"].label = "Middle name (optional)" - # # Set custom error messages - # self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."} - # self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."} - # self.fields["title"].error_messages = { - # "required": "Enter your title or role in your organization (e.g., Chief Information Officer)" - # } - # self.fields["email"].error_messages = { - # "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 \ No newline at end of file + # Set custom error messages + self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."} + self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."} + self.fields["title"].error_messages = { + "required": "Enter your title or role in your organization (e.g., Chief Information Officer)" + } + self.fields["email"].error_messages = { + "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 \ No newline at end of file diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 4756f5976..efaf46dfa 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -158,7 +158,7 @@
  • | - Your profile + Your profile
  • | diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index b6ec2289d..3530cccb7 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -33,7 +33,8 @@ class UserProfileView(UserProfilePermissionView): def get(self, request, *args, **kwargs): logger.info("in get()") self.object = self.get_object() - context = self.get_context_data(object=self.object) + form = self.form_class(instance=self.object) + context = self.get_context_data(object=self.object, form=form) logger.info(context) return self.render_to_response(context) @@ -44,11 +45,11 @@ class UserProfileView(UserProfilePermissionView): # kwargs.update(self.extra_context) # return kwargs - # # Override get_object to return the logged-in user - # def get_object(self, queryset=None): - # logger.info("in get_object") - # user = self.request.user # get the logged in user - # if hasattr(user, 'contact'): # Check if the user has a contact instance - # logger.info(user.contact) - # return user.contact - # return None \ No newline at end of file + # Override get_object to return the logged-in user + def get_object(self, queryset=None): + logger.info("in get_object") + user = self.request.user # get the logged in user + if hasattr(user, 'contact'): # Check if the user has a contact instance + logger.info(user.contact) + return user.contact + return None \ No newline at end of file From f34da8029a8cb14c5bb93508ec936fb37a79d755 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 14 May 2024 18:35:58 -0400 Subject: [PATCH 07/35] Active class on Your profile link --- src/registrar/templates/base.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index efaf46dfa..03b7c0f2a 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -134,7 +134,7 @@ {% block usa_overlay %}
    {% endblock %} {% block banner %} -
    +
    {% block logo %} @@ -147,7 +147,7 @@
    {% block usa_nav %} -
  • | - Your profile + {% url 'user-profile' as user_profile_url %} +
  • -
  • +
  • | Sign out {% else %} From b77b5ae689de457f46702146fdf5b4b7c4064755 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 15 May 2024 08:13:19 -0400 Subject: [PATCH 08/35] working code for form submission --- src/registrar/forms/user_profile.py | 5 +++- src/registrar/templates/profile.html | 11 +++++++ src/registrar/views/user_profile.py | 45 +++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index fe4d6d609..c04baa9e5 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -4,6 +4,7 @@ from registrar.models.contact import Contact from django.core.validators import MaxLengthValidator from phonenumber_field.widgets import RegionalPhoneNumberWidget +from registrar.models.utility.domain_helper import DomainHelper class UserProfileForm(forms.ModelForm): """Form for updating user profile.""" @@ -51,4 +52,6 @@ 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 \ No newline at end of file + self.domainInfo = None + + DomainHelper.disable_field(self.fields["email"], disable_required=True) \ No newline at end of file diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index e91a68042..c4c5c5db6 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -19,6 +19,17 @@ Edit your User Profile | Back to manage your domains

    + {# messages block is under the back breadcrumb link #} + {% if messages %} + {% for message in messages %} +
    +
    + {{ message }} +
    +
    + {% endfor %} + {% endif %} + {% include "includes/form_errors.html" with form=form %}

    Your profile

    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.

    Contact information

    diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 3530cccb7..ea1f1ffdf 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -4,7 +4,10 @@ import logging +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.models import ( User, Contact, @@ -15,7 +18,7 @@ from registrar.views.utility.permission_views import UserProfilePermissionView logger = logging.getLogger(__name__) -class UserProfileView(UserProfilePermissionView): +class UserProfileView(UserProfilePermissionView, FormMixin): """ Base View for the Domain. Handles getting and setting the domain in session cache on GETs. Also provides methods for getting @@ -38,13 +41,39 @@ class UserProfileView(UserProfilePermissionView): logger.info(context) return self.render_to_response(context) - # def get_context_data(self, **kwargs): - # logger.info("in get_context_data") - # kwargs.setdefault("view", self) - # if self.extra_context is not None: - # kwargs.update(self.extra_context) - # return kwargs - + def get_success_url(self): + """Redirect to the overview page for the domain.""" + return reverse("user-profile") + + # def post(self, request, *args, **kwargs): + # # Handle POST request logic here + # form = self.get_form() + # if form.is_valid(): + # # Save form data or perform other actions + # return HttpResponseRedirect(reverse('profile_success')) # Redirect to a success page + # else: + # # Form is not valid, re-render the page with errors + # return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form = self.get_form() + form.instance.id = self.object.id + form.instance.created_at = self.object.created_at + form.instance.user = self.request.user + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + form.save() + + messages.success(self.request, "Your profile has been updated.") + + # superclass has the redirect + return super().form_valid(form) + # Override get_object to return the logged-in user def get_object(self, queryset=None): logger.info("in get_object") From 58fe2524f338c53de9bc6c27696e67ac566a638e Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Wed, 15 May 2024 15:00:21 -0400 Subject: [PATCH 09/35] wip --- src/registrar/config/urls.py | 1 + src/registrar/forms/user_profile.py | 1 + src/registrar/templates/base.html | 2 ++ src/registrar/templates/domain_detail.html | 2 ++ src/registrar/templates/domain_sidebar.html | 2 ++ src/registrar/views/domain.py | 20 +++++++++++++- src/registrar/views/domain_request.py | 21 +++++++++++++-- src/registrar/views/user_profile.py | 29 +++++++-------------- src/registrar/views/utility/error_views.py | 21 ++++++++++----- 9 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 380cdb803..158f8e812 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -211,6 +211,7 @@ urlpatterns = [ # Rather than dealing with that, we keep everything centralized in one location. # This way, we can share a view for djangooidc, and other pages as we see fit. handler500 = "registrar.views.utility.error_views.custom_500_error_view" +handler403 = "registrar.views.utility.error_views.custom_403_error_view" # we normally would guard these with `if settings.DEBUG` but tests run with # DEBUG = False even when these apps have been loaded because settings.DEBUG diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index c04baa9e5..c825c6352 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -51,6 +51,7 @@ class UserProfileForm(forms.ModelForm): self.fields["email"].error_messages = { "required": "Enter your email address in the required format, like name@example.com." } + # self.fields["email"].widget.attrs["readonly"] = "readonly" self.fields["phone"].error_messages["required"] = "Enter your phone number." self.domainInfo = None diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 03b7c0f2a..d3a7e3e48 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -156,6 +156,7 @@ {% if user.is_authenticated %} {{ user.email }}
  • + {% if has_profile_feature_flag %}
  • | {% url 'user-profile' as user_profile_url %} @@ -163,6 +164,7 @@ Your profile
  • + {% endif %}
  • | Sign out diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 2b2d45695..67837196f 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -59,8 +59,10 @@ {% url 'domain-authorizing-official' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url editable=domain.is_editable %} + {% if not has_profile_feature_flag %} {% url 'domain-your-contact-information' pk=domain.id as url %} {% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %} + {% endif %} {% url 'domain-security-email' pk=domain.id as url %} {% if security_email is not None and security_email not in hidden_security_emails%} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 9e00fafa9..7453909a8 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -73,6 +73,7 @@
  • + {% if not has_profile_feature_flag %}
  • {% url 'domain-your-contact-information' pk=domain.id as url %}
  • + {% endif %}
  • {% url 'domain-security-email' pk=domain.id as url %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 9134080a1..2dc096083 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -59,7 +59,7 @@ from epplibwrapper import ( from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView - +from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -102,6 +102,13 @@ class DomainBaseView(DomainPermissionView): domain_pk = "domain:" + str(self.kwargs.get("pk")) self.session[domain_pk] = self.object + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # This is a django waffle flag which toggles features based off of the "flag" table + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + return context + class DomainFormBaseView(DomainBaseView, FormMixin): """ @@ -588,6 +595,17 @@ class DomainYourContactInformationView(DomainFormBaseView): # superclass has the redirect return super().form_valid(form) + + def has_permission(self): + """Check if this user has access to this domain. + + The user is in self.request.user and the domain needs to be looked + up from the domain's primary key in self.kwargs["pk"] + """ + if flag_is_active(self.request, "profile_feature"): + return False + + return super().has_permission() class DomainSecurityEmailView(DomainFormBaseView): diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f93976138..5d1e234a6 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -22,6 +22,8 @@ from .utility import ( DomainRequestWizardPermissionView, ) +from waffle.decorators import flag_is_active + logger = logging.getLogger(__name__) @@ -225,14 +227,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): # will NOT be redirected. The purpose of this is to allow code to # send users "to the domain request wizard" without needing to # know which view is first in the list of steps. + context = self.get_context_data() if self.__class__ == DomainRequestWizard: if request.path_info == self.NEW_URL_NAME: - return render(request, "domain_request_intro.html") + return render(request, "domain_request_intro.html", context) else: return self.goto(self.steps.first) self.steps.current = current_url - context = self.get_context_data() context["forms"] = self.get_forms() # if pending requests exist and user does not have approved domains, @@ -392,6 +394,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "is_federal": self.domain_request.is_federal(), "modal_button": modal_button, "modal_heading": modal_heading, + "has_profile_feature_flag": flag_is_active(self.request, "profile_feature") } def get_step_list(self) -> list: @@ -695,6 +698,13 @@ class Finished(DomainRequestWizard): class DomainRequestStatus(DomainRequestPermissionView): template_name = "domain_request_status.html" + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # This is a django waffle flag which toggles features based off of the "flag" table + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + return context + class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView): """This page will ask user to confirm if they want to withdraw @@ -705,6 +715,13 @@ class DomainRequestWithdrawConfirmation(DomainRequestPermissionWithdrawView): template_name = "domain_request_withdraw_confirmation.html" + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # This is a django waffle flag which toggles features based off of the "flag" table + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + return context + class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView): # this view renders no template diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index ea1f1ffdf..cddb51e48 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -13,7 +13,7 @@ from registrar.models import ( Contact, ) from registrar.views.utility.permission_views import UserProfilePermissionView - +from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -29,10 +29,6 @@ class UserProfileView(UserProfilePermissionView, FormMixin): template_name = "profile.html" form_class = UserProfileForm - # def get(self, request, *args, **kwargs): - # logger.info("in get") - # return super().get(request, *args, **kwargs) - def get(self, request, *args, **kwargs): logger.info("in get()") self.object = self.get_object() @@ -41,26 +37,21 @@ class UserProfileView(UserProfilePermissionView, FormMixin): logger.info(context) return self.render_to_response(context) + def get_context_data(self, **kwargs): + """Adjust context from FormMixin for formsets.""" + context = super().get_context_data(**kwargs) + # This is a django waffle flag which toggles features based off of the "flag" table + context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature") + return context + def get_success_url(self): """Redirect to the overview page for the domain.""" return reverse("user-profile") - # def post(self, request, *args, **kwargs): - # # Handle POST request logic here - # form = self.get_form() - # if form.is_valid(): - # # Save form data or perform other actions - # return HttpResponseRedirect(reverse('profile_success')) # Redirect to a success page - # else: - # # Form is not valid, re-render the page with errors - # return self.render_to_response(self.get_context_data(form=form)) - def post(self, request, *args, **kwargs): self.object = self.get_object() - form = self.get_form() - form.instance.id = self.object.id - form.instance.created_at = self.object.created_at - form.instance.user = self.request.user + form = self.form_class(request.POST, instance=self.object) + if form.is_valid(): return self.form_valid(form) else: diff --git a/src/registrar/views/utility/error_views.py b/src/registrar/views/utility/error_views.py index 48ae628a4..2374277d5 100644 --- a/src/registrar/views/utility/error_views.py +++ b/src/registrar/views/utility/error_views.py @@ -14,19 +14,28 @@ Rather than dealing with that, we keep everything centralized in one location. """ from django.shortcuts import render +from waffle.decorators import flag_is_active def custom_500_error_view(request, context=None): """Used to redirect 500 errors to a custom view""" if context is None: - return render(request, "500.html", status=500) - else: - return render(request, "500.html", context=context, status=500) + context = {} + context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") + return render(request, "500.html", context=context, status=500) def custom_401_error_view(request, context=None): """Used to redirect 401 errors to a custom view""" if context is None: - return render(request, "401.html", status=401) - else: - return render(request, "401.html", context=context, status=401) + context = {} + context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") + return render(request, "401.html", context=context, status=401) + + +def custom_403_error_view(request, exception=None, context=None): + """Used to redirect 403 errors to a custom view""" + if context is None: + context = {} + context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature") + return render(request, "403.html", context=context, status=403) From 0a0a251d771e0d94a2516f433dd7f140c7c42627 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 15 May 2024 15:40:32 -0400 Subject: [PATCH 10/35] fix nav --- src/registrar/assets/sass/_theme/_base.scss | 31 +++++++++++++++++++++ src/registrar/templates/base.html | 10 +++---- src/registrar/templates/profile.html | 3 +- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 212df992f..de5a200c5 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -46,6 +46,37 @@ body { margin-top:units(1); } +.usa-nav__primary-username { + display: inline-block; + padding: units(1) units(2); + max-width: 208px; + overflow: hidden; + text-overflow: ellipsis; + @include at-media(desktop) { + padding: units(2); + max-width: 500px; + } +} + +@include at-media(desktop) { + + .usa-nav__primary-item:not(:first-child) { + position: relative; + } + + .usa-nav__primary-item:not(:first-child)::before { + content: ''; + position: absolute; + top: 50%; + left: 0; + width: 0; /* No width since it's a border */ + height: 50%; + border-left: solid 1px color('base-light'); + transform: translateY(-50%); + } + +} + .section--outlined { background-color: color('white'); border: 1px solid color('base-lighter'); diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index d3a7e3e48..71f89fc14 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -151,22 +151,20 @@ -