diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml
index f2b4303d6..84f228893 100644
--- a/.github/workflows/deploy-sandbox.yaml
+++ b/.github/workflows/deploy-sandbox.yaml
@@ -27,6 +27,7 @@ jobs:
|| startsWith(github.head_ref, 'cb/')
|| startsWith(github.head_ref, 'hotgov/')
|| startsWith(github.head_ref, 'litterbox/')
+ || startsWith(github.head_ref, 'ag/')
outputs:
environment: ${{ steps.var.outputs.environment}}
runs-on: "ubuntu-latest"
diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml
index 283380236..81368f6e9 100644
--- a/.github/workflows/migrate.yaml
+++ b/.github/workflows/migrate.yaml
@@ -16,6 +16,7 @@ on:
- stable
- staging
- development
+ - ag
- litterbox
- hotgov
- cb
diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml
index b9393415b..ad325c50a 100644
--- a/.github/workflows/reset-db.yaml
+++ b/.github/workflows/reset-db.yaml
@@ -16,6 +16,7 @@ on:
options:
- staging
- development
+ - ag
- litterbox
- hotgov
- cb
diff --git a/ops/manifests/manifest-ag.yaml b/ops/manifests/manifest-ag.yaml
new file mode 100644
index 000000000..68d630f3e
--- /dev/null
+++ b/ops/manifests/manifest-ag.yaml
@@ -0,0 +1,32 @@
+---
+applications:
+- name: getgov-ag
+ buildpacks:
+ - python_buildpack
+ path: ../../src
+ instances: 1
+ memory: 512M
+ stack: cflinuxfs4
+ timeout: 180
+ command: ./run.sh
+ health-check-type: http
+ health-check-http-endpoint: /health
+ health-check-invocation-timeout: 40
+ env:
+ # Send stdout and stderr straight to the terminal without buffering
+ PYTHONUNBUFFERED: yup
+ # Tell Django where to find its configuration
+ DJANGO_SETTINGS_MODULE: registrar.config.settings
+ # Tell Django where it is being hosted
+ DJANGO_BASE_URL: https://getgov-ag.app.cloud.gov
+ # Tell Django how much stuff to log
+ DJANGO_LOG_LEVEL: INFO
+ # default public site location
+ GETGOV_PUBLIC_SITE_URL: https://get.gov
+ # Flag to disable/enable features in prod environments
+ IS_PRODUCTION: False
+ routes:
+ - route: getgov-ag.app.cloud.gov
+ services:
+ - getgov-credentials
+ - getgov-ag-database
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index cc11f1336..86bb1fe6e 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -1604,6 +1604,18 @@ document.addEventListener('DOMContentLoaded', function() {
+/**
+ * An IIFE that displays confirmation modal on the user profile page
+ */
+(function userProfileListener() {
+
+ const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal');
+ if (showConfirmationModalTrigger) {
+ showConfirmationModalTrigger.click();
+ }
+}
+)();
+
/**
* An IIFE that hooks up the edit buttons on the finish-user-setup page
*/
diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss
index e88d75f4e..e5e8b89ee 100644
--- a/src/registrar/assets/sass/_theme/_base.scss
+++ b/src/registrar/assets/sass/_theme/_base.scss
@@ -70,7 +70,7 @@ body {
top: 50%;
left: 0;
width: 0; /* No width since it's a border */
- height: 50%;
+ height: 40%;
border-left: solid 1px color('base-light');
transform: translateY(-50%);
}
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index 9a6792dc7..8438812c4 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -659,6 +659,7 @@ ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov",
+ "getgov-ag.app.cloud.gov",
"getgov-litterbox.app.cloud.gov",
"getgov-hotgov.app.cloud.gov",
"getgov-cb.app.cloud.gov",
diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py
index 557e34e0d..3dd8cbdce 100644
--- a/src/registrar/forms/user_profile.py
+++ b/src/registrar/forms/user_profile.py
@@ -47,7 +47,7 @@ class UserProfileForm(forms.ModelForm):
self.fields["middle_name"].label = "Middle name (optional)"
self.fields["last_name"].label = "Last name / family name"
self.fields["title"].label = "Title or role in your organization"
- self.fields["email"].label = "Organizational email"
+ self.fields["email"].label = "Organization email"
# Set custom error messages
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py
index f9921513b..79e3b7a11 100644
--- a/src/registrar/registrar_middleware.py
+++ b/src/registrar/registrar_middleware.py
@@ -5,6 +5,7 @@ Contains middleware used in settings.py
from urllib.parse import parse_qs
from django.urls import reverse
from django.http import HttpResponseRedirect
+from registrar.models.user import User
from waffle.decorators import flag_is_active
from registrar.models.utility.generic_helper import replace_url_queryparams
@@ -38,10 +39,17 @@ class CheckUserProfileMiddleware:
self.get_response = get_response
self.setup_page = reverse("finish-user-profile-setup")
+ self.profile_page = reverse("user-profile")
self.logout_page = reverse("logout")
- self.excluded_pages = [
+ self.regular_excluded_pages = [
self.setup_page,
self.logout_page,
+ "/admin",
+ ]
+ self.other_excluded_pages = [
+ self.profile_page,
+ self.logout_page,
+ "/admin",
]
def __call__(self, request):
@@ -61,12 +69,15 @@ class CheckUserProfileMiddleware:
if request.user.is_authenticated:
if hasattr(request.user, "finished_setup") and not request.user.finished_setup:
- return self._handle_setup_not_finished(request)
+ if request.user.verification_type == User.VerificationTypeChoices.REGULAR:
+ return self._handle_regular_user_setup_not_finished(request)
+ else:
+ return self._handle_other_user_setup_not_finished(request)
# Continue processing the view
return None
- def _handle_setup_not_finished(self, request):
+ def _handle_regular_user_setup_not_finished(self, request):
"""Redirects the given user to the finish setup page.
We set the "redirect" query param equal to where the user wants to go.
@@ -82,7 +93,7 @@ class CheckUserProfileMiddleware:
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 self.excluded_pages):
+ if not any(request.path.startswith(page) for page in self.regular_excluded_pages):
# Preserve the original query parameters, and coerce them into a dict
query_params = parse_qs(request.META["QUERY_STRING"])
@@ -98,3 +109,13 @@ class CheckUserProfileMiddleware:
else:
# Process the view as normal
return None
+
+ def _handle_other_user_setup_not_finished(self, request):
+ """Redirects the given user to the profile page to finish setup."""
+
+ # Don't redirect on excluded pages (such as the setup page itself)
+ if not any(request.path.startswith(page) for page in self.other_excluded_pages):
+ return HttpResponseRedirect(self.profile_page)
+ else:
+ # Process the view as normal
+ return None
diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html
index 15261440d..f93159f01 100644
--- a/src/registrar/templates/home.html
+++ b/src/registrar/templates/home.html
@@ -15,7 +15,11 @@
{% endblock %}
Manage your domains
-
+ {% comment %}
+ IMPORTANT:
+ If this button is added on any other page, make sure to update the
+ relevant view to reset request.session["new_request"] = True
+ {% endcomment %}
+ .Gov domain registrants must maintain accurate contact information in the .gov registrar.
+ Before you can manage your domain, we need you to add your contact information.
+
+
+
+
+
+
+
+ {% endif %}
+
+
{% endblock content %}
@@ -43,3 +103,7 @@ Edit your User Profile |
{% endblock content_bottom %}
+
+{% block footer %}
+ {% include "includes/footer.html" with show_manage_your_domains=user_finished_setup %}
+{% endblock footer %}
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 90780c5da..fa63da17a 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -63,11 +63,24 @@ class TestWithUser(MockEppLib):
self.user.contact.title = title
self.user.contact.save()
- username_incomplete = "test_user_incomplete"
+ username_regular_incomplete = "test_regular_user_incomplete"
+ username_other_incomplete = "test_other_user_incomplete"
first_name_2 = "Incomplete"
email_2 = "unicorn@igorville.com"
- self.incomplete_user = get_user_model().objects.create(
- username=username_incomplete, first_name=first_name_2, email=email_2
+ # in the case below, REGULAR user is 'Verified by Login.gov, ie. IAL2
+ self.incomplete_regular_user = get_user_model().objects.create(
+ username=username_regular_incomplete,
+ first_name=first_name_2,
+ email=email_2,
+ verification_type=User.VerificationTypeChoices.REGULAR,
+ )
+ # in the case below, other user is representative of GRANDFATHERED,
+ # VERIFIED_BY_STAFF, INVITED, FIXTURE_USER, ie. IAL1
+ self.incomplete_other_user = get_user_model().objects.create(
+ username=username_other_incomplete,
+ first_name=first_name_2,
+ email=email_2,
+ verification_type=User.VerificationTypeChoices.VERIFIED_BY_STAFF,
)
def tearDown(self):
@@ -75,8 +88,7 @@ class TestWithUser(MockEppLib):
super().tearDown()
DomainRequest.objects.all().delete()
DomainInformation.objects.all().delete()
- self.user.delete()
- self.incomplete_user.delete()
+ User.objects.all().delete()
class TestEnvironmentVariablesEffects(TestCase):
@@ -526,7 +538,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
@less_console_noise_decorator
def test_new_user_with_profile_feature_on(self):
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
- self.app.set_user(self.incomplete_user.username)
+ self.app.set_user(self.incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page.
# Follow implicity checks if our redirect is working.
@@ -565,7 +577,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
- self.app.set_user(self.incomplete_user.username)
+ self.app.set_user(self.incomplete_regular_user.username)
with override_flag("profile_feature", active=True):
# This will redirect the user to the setup page
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
@@ -619,6 +631,106 @@ class FinishUserProfileTests(TestWithUser, WebTest):
self.assertContains(response, "You’re about to start your .gov domain request")
+class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
+ """A series of tests that target the user profile page intercept for incomplete IAL1 user profiles."""
+
+ # csrf checks do not work well with WebTest.
+ # We disable them here.
+ csrf_checks = False
+
+ def setUp(self):
+ super().setUp()
+ self.user.title = None
+ self.user.save()
+ self.client.force_login(self.user)
+ self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
+ self.role, _ = UserDomainRole.objects.get_or_create(
+ user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
+ )
+
+ def tearDown(self):
+ super().tearDown()
+ PublicContact.objects.filter(domain=self.domain).delete()
+ self.role.delete()
+ self.domain.delete()
+ Domain.objects.all().delete()
+ Website.objects.all().delete()
+ Contact.objects.all().delete()
+
+ def _set_session_cookie(self):
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+
+ def _submit_form_webtest(self, form, follow=False):
+ page = form.submit()
+ self._set_session_cookie()
+ return page.follow() if follow else page
+
+ @less_console_noise_decorator
+ def test_new_user_with_profile_feature_on(self):
+ """Tests that a new user is redirected to the profile setup page when profile_feature is on,
+ and testing that the confirmation modal is present"""
+ self.app.set_user(self.incomplete_other_user.username)
+ with override_flag("profile_feature", active=True):
+ # This will redirect the user to the user profile page.
+ # Follow implicity checks if our redirect is working.
+ user_profile_page = self.app.get(reverse("home")).follow()
+ self._set_session_cookie()
+
+ # Assert that we're on the right page by testing for the modal
+ self.assertContains(user_profile_page, "domain registrants must maintain accurate contact information")
+
+ user_profile_page = self._submit_form_webtest(user_profile_page.form)
+
+ self.assertEqual(user_profile_page.status_code, 200)
+
+ # Assert that modal does not appear on subsequent submits
+ self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
+ # Assert that unique error message appears by testing the message in a specific div
+ html_content = user_profile_page.content.decode("utf-8")
+ # Normalize spaces and line breaks in the HTML content
+ normalized_html_content = " ".join(html_content.split())
+ # Expected string without extra spaces and line breaks
+ expected_string = "Before you can manage your domain, we need you to add contact information."
+ # Check for the presence of the
element with the specific text
+ self.assertIn(f'
{expected_string}
', normalized_html_content)
+
+ # We're missing a phone number, so the page should tell us that
+ self.assertContains(user_profile_page, "Enter your phone number.")
+
+ # We need to assert that links to manage your domain are not present (in both body and footer)
+ self.assertNotContains(user_profile_page, "Manage your domains")
+ # Assert the tooltip on the logo, indicating that the logo is not clickable
+ self.assertContains(
+ user_profile_page, 'title="Before you can manage your domains, we need you to add contact information."'
+ )
+ # Assert that modal does not appear on subsequent submits
+ self.assertNotContains(user_profile_page, "domain registrants must maintain accurate contact information")
+
+ # Add a phone number
+ finish_setup_form = user_profile_page.form
+ finish_setup_form["phone"] = "(201) 555-0123"
+ finish_setup_form["title"] = "CEO"
+ finish_setup_form["last_name"] = "example"
+ save_page = self._submit_form_webtest(finish_setup_form, follow=True)
+
+ self.assertEqual(save_page.status_code, 200)
+ self.assertContains(save_page, "Your profile has been updated.")
+
+ # We need to assert that logo is not clickable and links to manage your domain are not present
+ self.assertContains(save_page, "anage your domains", count=2)
+ self.assertNotContains(
+ save_page, "Before you can manage your domains, we need you to add contact information"
+ )
+ # Assert that modal does not appear on subsequent submits
+ self.assertNotContains(save_page, "domain registrants must maintain accurate contact information")
+
+ # Try to navigate back to the home page.
+ # This is the same as clicking the back button.
+ completed_setup_page = self.app.get(reverse("home"))
+ self.assertContains(completed_setup_page, "Manage your domain")
+
+
class UserProfileTests(TestWithUser, WebTest):
"""A series of tests that target your profile functionality"""
diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py
index b46b89b9a..69d222cce 100644
--- a/src/registrar/tests/test_views_request.py
+++ b/src/registrar/tests/test_views_request.py
@@ -102,6 +102,58 @@ class DomainRequestTests(TestWithUser, WebTest):
self.assertContains(type_page, "You cannot submit this request yet")
+ def test_domain_request_into_acknowledgement_creates_new_request(self):
+ """
+ We had to solve a bug where the wizard was creating 2 requests on first intro acknowledgement ('continue')
+ The wizard was also creating multiiple requests on 'continue' -> back button -> 'continue' etc.
+
+ This tests that the domain requests get created only when they should.
+ """
+ # Get the intro page
+ self.app.get(reverse("home"))
+ session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ intro_page = self.app.get(reverse("domain-request:"))
+
+ # Select the form
+ intro_form = intro_page.forms[0]
+
+ # Submit the form, this creates 1 Request
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ response = intro_form.submit(name="submit_button", value="intro_acknowledge")
+
+ # Landing on the next page used to create another 1 request
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ response.follow()
+
+ # Check if a new DomainRequest object has been created
+ domain_request_count = DomainRequest.objects.count()
+ self.assertEqual(domain_request_count, 1)
+
+ # Let's go back to intro and submit again, this should not create a new request
+ # This is the equivalent of a back button nav from step 1 to intro -> continue
+ intro_form = intro_page.forms[0]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ type_form = intro_form.submit(name="submit_button", value="intro_acknowledge")
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ type_form.follow()
+ domain_request_count = DomainRequest.objects.count()
+ self.assertEqual(domain_request_count, 1)
+
+ # Go home, which will reset the session flag for new request
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ self.app.get(reverse("home"))
+
+ # This time, clicking continue will create a new request
+ intro_form = intro_page.forms[0]
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ intro_result = intro_form.submit(name="submit_button", value="intro_acknowledge")
+ self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+ intro_result.follow()
+ domain_request_count = DomainRequest.objects.count()
+ self.assertEqual(domain_request_count, 2)
+
@boto3_mocking.patching
def test_domain_request_form_submission(self):
"""
diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py
index bd8431204..77699e17a 100644
--- a/src/registrar/views/domain_request.py
+++ b/src/registrar/views/domain_request.py
@@ -219,22 +219,23 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
self.storage["domain_request_id"] = kwargs["id"]
self.storage["step_history"] = self.db_check_for_unlocking_steps()
- # if accessing this class directly, redirect to the first step
- # in other words, if `DomainRequestWizard` is called as view
- # directly by some redirect or url handler, we'll send users
- # either to an acknowledgement page or to the first step in
- # the processes (if an edit rather than a new request); subclasses
- # 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 accessing this class directly, redirect to either to an acknowledgement
+ # page or to the first step in the processes (if an edit rather than a new request);
+ # subclasseswill 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.
if self.__class__ == DomainRequestWizard:
if request.path_info == self.NEW_URL_NAME:
- context = self.get_context_data()
- return render(request, "domain_request_intro.html", context=context)
+ # Clear context so the prop getter won't create a request here.
+ # Creating a request will be handled in the post method for the
+ # intro page. Only TEMPORARY context needed is has_profile_flag
+ has_profile_flag = flag_is_active(self.request, "profile_feature")
+ context_stuff = {"has_profile_feature_flag": has_profile_flag}
+ return render(request, "domain_request_intro.html", context=context_stuff)
else:
return self.goto(self.steps.first)
+ context = self.get_context_data()
self.steps.current = current_url
context["forms"] = self.get_forms()
@@ -434,6 +435,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
return step_list
def goto(self, step):
+ if step == "generic_org_type":
+ # We need to avoid creating a new domain request if the user
+ # clicks the back button
+ self.request.session["new_request"] = False
self.steps.current = step
return redirect(reverse(f"{self.URL_NAMESPACE}:{step}"))
@@ -456,11 +461,17 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
# which button did the user press?
button: str = request.POST.get("submit_button", "")
-
+ # If a user hits the new request url directly
+ if "new_request" not in request.session:
+ request.session["new_request"] = True
# if user has acknowledged the intro message
if button == "intro_acknowledge":
if request.path_info == self.NEW_URL_NAME:
- del self.storage
+
+ if self.request.session["new_request"] is True:
+ # This will trigger the domain_request getter into creating a new DomainRequest
+ del self.storage
+
return self.goto(self.steps.first)
# if accessing this class directly, redirect to the first step
diff --git a/src/registrar/views/index.py b/src/registrar/views/index.py
index 54f315134..c05bde21d 100644
--- a/src/registrar/views/index.py
+++ b/src/registrar/views/index.py
@@ -10,4 +10,7 @@ def index(request):
# This is a django waffle flag which toggles features based off of the "flag" table
context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
+ # This controls the creation of a new domain request in the wizard
+ request.session["new_request"] = True
+
return render(request, "home.html", context)
diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py
index 47b01f7cb..f148f5652 100644
--- a/src/registrar/views/user_profile.py
+++ b/src/registrar/views/user_profile.py
@@ -15,6 +15,7 @@ from django.urls import NoReverseMatch, reverse
from registrar.models import (
Contact,
)
+from registrar.models.user import User
from registrar.models.utility.generic_helper import replace_url_queryparams
from registrar.views.utility.permission_views import UserProfilePermissionView
from waffle.decorators import flag_is_active, waffle_flag
@@ -41,6 +42,13 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
form = self.form_class(instance=self.object)
context = self.get_context_data(object=self.object, form=form)
+ if (
+ hasattr(self.user, "finished_setup")
+ and not self.user.finished_setup
+ and self.user.verification_type != User.VerificationTypeChoices.REGULAR
+ ):
+ context["show_confirmation_modal"] = True
+
return_to_request = request.GET.get("return_to_request")
if return_to_request:
context["return_to_request"] = True
@@ -67,7 +75,11 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
# The text for the back button on this page
context["profile_back_button_text"] = "Go to manage your domains"
- context["show_back_button"] = True
+ context["show_back_button"] = False
+
+ if hasattr(self.user, "finished_setup") and self.user.finished_setup:
+ context["user_finished_setup"] = True
+ context["show_back_button"] = True
return context
@@ -94,6 +106,12 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
else:
return self.form_invalid(form)
+ def form_invalid(self, form):
+ """If the form is invalid, conditionally display an additional error."""
+ if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
+ messages.error(self.request, "Before you can manage your domain, we need you to add contact information.")
+ return super().form_invalid(form)
+
def form_valid(self, form):
"""Handle successful and valid form submissions."""
form.save()
@@ -105,9 +123,9 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
def get_object(self, queryset=None):
"""Override get_object to return the logged-in user's contact"""
- user = self.request.user # get the logged in user
- if hasattr(user, "contact"): # Check if the user has a contact instance
- return user.contact
+ self.user = self.request.user # get the logged in user
+ if hasattr(self.user, "contact"): # Check if the user has a contact instance
+ return self.user.contact
return None