mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 20:18:38 +02:00
removed user-contact connection
This commit is contained in:
parent
694cfa543b
commit
6361369953
22 changed files with 129 additions and 418 deletions
|
@ -353,49 +353,6 @@ cf env getgov-{app name}
|
|||
|
||||
Then, copy the variables under the section labled `s3`.
|
||||
|
||||
## Signals
|
||||
The application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, it uses a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
|
||||
|
||||
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)"
|
||||
|
||||
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
|
||||
|
||||
### Rules of use
|
||||
When using signals, try to adhere to these guidelines:
|
||||
1. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
|
||||
2. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
|
||||
3. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
|
||||
4. Minimize logic complexity within the signal as much as possible.
|
||||
|
||||
### When should you use signals?
|
||||
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
|
||||
|
||||
However, in most scenarios, if you can get away with avoiding signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
|
||||
|
||||
Consider using signals when:
|
||||
1. Synchronizing events across multiple models or areas of code.
|
||||
2. Performing logic before or after saving a model to the database (when otherwise difficult through `save()`).
|
||||
3. Encountering an import loop when overriding functions such as `save()`.
|
||||
4. You are otherwise unable to achieve the intended behavior by overrides or other means.
|
||||
5. (Rare) Offloading tasks when multi-threading.
|
||||
|
||||
For the vast majority of use cases, the [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) and [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) signals are sufficient in terms of model-to-model management.
|
||||
|
||||
### Where should you use them?
|
||||
This project compiles signals in a unified location to maintain readability. If you are adding a signal or otherwise utilizing one, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
|
||||
|
||||
### How are we currently using signals?
|
||||
At the time of writing, we currently only use signals for the Contact and User objects when synchronizing data returned from Login.gov. This is because the `Contact` object holds information that the user specified in our system, whereas the `User` object holds information that was specified in Login.gov.
|
||||
|
||||
To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
|
||||
|
||||
#### handle_profile
|
||||
This function is triggered by the post_save event on the User model, designed to manage the synchronization between User and Contact entities. It operates under the following conditions:
|
||||
|
||||
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
||||
|
||||
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
||||
|
||||
## Disable email sending (toggling the disable_email_sending flag)
|
||||
1. On the app, navigate to `\admin`.
|
||||
2. Under models, click `Waffle flags`.
|
||||
|
|
|
@ -5,12 +5,3 @@ class RegistrarConfig(AppConfig):
|
|||
"""Configure signal handling for our registrar Django application."""
|
||||
|
||||
name = "registrar"
|
||||
|
||||
def ready(self):
|
||||
"""Runs when all Django applications have been loaded.
|
||||
|
||||
We use it here to load signals that connect related models.
|
||||
"""
|
||||
# noqa here because we are importing something to make the signals
|
||||
# get registered, but not using what we import
|
||||
from . import signals # noqa
|
||||
|
|
|
@ -4,7 +4,7 @@ from .domain import (
|
|||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
DomainOrgNameAddressForm,
|
||||
ContactForm,
|
||||
UserForm,
|
||||
AuthorizingOfficialContactForm,
|
||||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
|
|
|
@ -16,7 +16,7 @@ from registrar.utility.errors import (
|
|||
SecurityEmailErrorCodes,
|
||||
)
|
||||
|
||||
from ..models import Contact, DomainInformation, Domain
|
||||
from ..models import Contact, DomainInformation, Domain, User
|
||||
from .common import (
|
||||
ALGORITHM_CHOICES,
|
||||
DIGEST_TYPE_CHOICES,
|
||||
|
@ -203,6 +203,63 @@ NameserverFormset = formset_factory(
|
|||
)
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
"""Form for updating contacts."""
|
||||
|
||||
email = forms.EmailField(max_length=None)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
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
|
||||
|
||||
def set_domain_info(self, domainInfo):
|
||||
"""Set the domain information for the form.
|
||||
The form instance is associated with the contact itself. In order to access the associated
|
||||
domain information object, this needs to be set in the form by the view."""
|
||||
self.domainInfo = domainInfo
|
||||
|
||||
|
||||
class ContactForm(forms.ModelForm):
|
||||
"""Form for updating contacts."""
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django import forms
|
||||
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
@ -13,7 +13,7 @@ class UserProfileForm(forms.ModelForm):
|
|||
redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Contact
|
||||
model = User
|
||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||
widgets = {
|
||||
"first_name": forms.TextInput,
|
||||
|
|
|
@ -65,13 +65,6 @@ class Command(BaseCommand):
|
|||
|
||||
resourcename = f"{table_name}Resource"
|
||||
|
||||
# if table_name is Contact, clean the table first
|
||||
# User table is loaded before Contact, and signals create
|
||||
# rows in Contact table which break the import, so need
|
||||
# to be cleaned again before running import on Contact table
|
||||
if table_name == "Contact":
|
||||
self.clean_table(table_name)
|
||||
|
||||
# Define the directory and the pattern for csv filenames
|
||||
tmp_dir = "tmp"
|
||||
pattern = f"{table_name}_"
|
||||
|
|
|
@ -8,13 +8,6 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
|||
class Contact(TimeStampedModel):
|
||||
"""
|
||||
Contact information follows a similar pattern for each contact.
|
||||
|
||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||
When a new user is created through Login.gov, a contact object will be created and
|
||||
associated on the `user` field.
|
||||
|
||||
If the `user` object already exists, the underlying user object
|
||||
will be updated if any updates are made to it through Login.gov.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -23,10 +23,6 @@ class User(AbstractUser):
|
|||
A custom user model that performs identically to the default user model
|
||||
but can be customized later.
|
||||
|
||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
||||
When a new user is created through Login.gov, a contact object will be created and
|
||||
associated on the contacts `user` field.
|
||||
|
||||
If the `user` object already exists, said user object
|
||||
will be updated if any updates are made to it through Login.gov.
|
||||
"""
|
||||
|
@ -113,15 +109,11 @@ class User(AbstractUser):
|
|||
Tracks if the user finished their profile setup or not. This is so
|
||||
we can globally enforce that new users provide additional account information before proceeding.
|
||||
"""
|
||||
|
||||
# Change this to self once the user and contact objects are merged.
|
||||
# For now, since they are linked, lets test on the underlying contact object.
|
||||
user_info = self.contact # noqa
|
||||
user_values = [
|
||||
user_info.first_name,
|
||||
user_info.last_name,
|
||||
user_info.title,
|
||||
user_info.phone,
|
||||
self.first_name,
|
||||
self.last_name,
|
||||
self.title,
|
||||
self.phone,
|
||||
]
|
||||
return None not in user_values
|
||||
|
||||
|
@ -169,8 +161,13 @@ class User(AbstractUser):
|
|||
"""Return count of ineligible requests"""
|
||||
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
|
||||
|
||||
def get_formatted_name(self):
|
||||
"""Returns the contact's name in Western order."""
|
||||
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
|
||||
return " ".join(names) if names else "Unknown"
|
||||
|
||||
def has_contact_info(self):
|
||||
return bool(self.contact.title or self.contact.email or self.contact.phone)
|
||||
return bool(self.title or self.email or self.phone)
|
||||
|
||||
@classmethod
|
||||
def needs_identity_verification(cls, email, uuid):
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import logging
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import User, Contact
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def handle_profile(sender, instance, **kwargs):
|
||||
"""Method for when a User is saved.
|
||||
|
||||
A first time registrant may have been invited, so we'll search for a matching
|
||||
Contact record, by email address, and associate them, if possible.
|
||||
|
||||
A first time registrant may not have a matching Contact, so we'll create one,
|
||||
copying the contact values we received from Login.gov in order to initialize it.
|
||||
|
||||
During subsequent login, a User record may be updated with new data from Login.gov,
|
||||
but in no case will we update contact values on an existing Contact record.
|
||||
"""
|
||||
|
||||
first_name = getattr(instance, "first_name", "")
|
||||
middle_name = getattr(instance, "middle_name", "")
|
||||
last_name = getattr(instance, "last_name", "")
|
||||
email = getattr(instance, "email", "")
|
||||
phone = getattr(instance, "phone", "")
|
||||
title = getattr(instance, "title", "")
|
||||
|
||||
is_new_user = kwargs.get("created", False)
|
||||
|
||||
if is_new_user:
|
||||
contacts = Contact.objects.filter(email=email)
|
||||
else:
|
||||
contacts = Contact.objects.filter(user=instance)
|
||||
|
||||
if len(contacts) == 0: # no matching contact
|
||||
Contact.objects.create(
|
||||
user=instance,
|
||||
first_name=first_name,
|
||||
middle_name=middle_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
phone=phone,
|
||||
title=title,
|
||||
)
|
||||
|
||||
if len(contacts) >= 1 and is_new_user: # a matching contact
|
||||
contacts[0].user = instance
|
||||
contacts[0].save()
|
||||
|
||||
if len(contacts) > 1: # multiple matches
|
||||
logger.warning(
|
||||
"There are multiple Contacts with the same email address."
|
||||
f" Picking #{contacts[0].id} for User #{instance.id}."
|
||||
)
|
|
@ -12,37 +12,24 @@
|
|||
|
||||
{% if user.has_contact_info %}
|
||||
{# Title #}
|
||||
{% if user.title or user.contact.title %}
|
||||
{% if user.contact.title %}
|
||||
{{ user.contact.title }}
|
||||
{% else %}
|
||||
{{ user.title }}
|
||||
{% endif %}
|
||||
{% if user.title %}
|
||||
{{ user.title }}
|
||||
<br>
|
||||
{% else %}
|
||||
None<br>
|
||||
{% endif %}
|
||||
{# Email #}
|
||||
{% if user.email or user.contact.email %}
|
||||
{% if user.contact.email %}
|
||||
{{ user.contact.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% else %}
|
||||
{{ user.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
{% endif %}
|
||||
{% if user.email %}
|
||||
{{ user.email }}
|
||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||
<br class="admin-icon-group__br">
|
||||
{% else %}
|
||||
None<br>
|
||||
{% endif %}
|
||||
|
||||
{# Phone #}
|
||||
{% if user.phone or user.contact.phone %}
|
||||
{% if user.contact.phone %}
|
||||
{{ user.contact.phone }}
|
||||
{% else %}
|
||||
{{ user.phone }}
|
||||
{% endif %}
|
||||
{% if user.phone %}
|
||||
{{ user.phone }}
|
||||
<br>
|
||||
{% else %}
|
||||
None<br>
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{# Conditionally display profile #}
|
||||
{% 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 %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=domain.is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
|
|
|
@ -77,11 +77,11 @@
|
|||
</fieldset>
|
||||
<div>
|
||||
|
||||
<button type="submit" name="contact_setup_save_button" class="usa-button ">
|
||||
<button type="submit" name="user_setup_save_button" class="usa-button ">
|
||||
Save
|
||||
</button>
|
||||
{% if user_finished_setup and going_to_specific_page %}
|
||||
<button type="submit" name="contact_setup_submit_button" class="usa-button usa-button--outline">
|
||||
<button type="submit" name="user_setup_submit_button" class="usa-button usa-button--outline">
|
||||
{{redirect_button_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
</h3>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul>
|
||||
<li>Full name: <b>{{ user.contact.get_formatted_name }}</b></li>
|
||||
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
||||
<li>Organization email: <b>{{ user.email }}</b></li>
|
||||
<li>Title or role in your organization: <b>{{ user.contact.title }}</b></li>
|
||||
<li>Phone: <b>{{ user.contact.phone.as_national }}</b></li>
|
||||
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
||||
<li>Phone: <b>{{ user.phone.as_national }}</b></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -810,6 +810,8 @@ def create_superuser():
|
|||
user = User.objects.create_user(
|
||||
username="superuser",
|
||||
email="admin@example.com",
|
||||
first_name="first",
|
||||
last_name="last",
|
||||
is_staff=True,
|
||||
password=p,
|
||||
)
|
||||
|
@ -826,6 +828,8 @@ def create_user():
|
|||
user = User.objects.create_user(
|
||||
username="staffuser",
|
||||
email="staff@example.com",
|
||||
first_name="first",
|
||||
last_name="last",
|
||||
is_staff=True,
|
||||
password=p,
|
||||
)
|
||||
|
|
|
@ -242,15 +242,11 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
domain_request.approve()
|
||||
|
@ -2067,15 +2063,11 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
|
||||
|
@ -2092,11 +2084,11 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
# Check for the right title and phone number in the response.
|
||||
# Email will appear more than once
|
||||
expected_creator_fields = [
|
||||
# Field, expected value
|
||||
("title", "Treat inspector"),
|
||||
("email", "meoward.jones@igorville.gov"),
|
||||
("phone", "(555) 123 12345"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||
|
@ -3103,15 +3095,11 @@ class TestDomainInformationAdmin(TestCase):
|
|||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Due to the relation between User <==> Contact,
|
||||
# the underlying contact has to be modified this way.
|
||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
||||
_creator.contact.phone = "(555) 123 12345"
|
||||
_creator.contact.title = "Treat inspector"
|
||||
_creator.contact.save()
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
domain_request.approve()
|
||||
|
@ -3133,13 +3121,12 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
# Check for the right title and phone number in the response.
|
||||
# We only need to check for the end tag
|
||||
# (Otherwise this test will fail if we change classes, etc)
|
||||
expected_creator_fields = [
|
||||
# Field, expected value
|
||||
("title", "Treat inspector"),
|
||||
("email", "meoward.jones@igorville.gov"),
|
||||
("phone", "(555) 123 12345"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||
|
@ -4067,8 +4054,8 @@ class TestContactAdmin(TestCase):
|
|||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_less(self):
|
||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins."""
|
||||
"""Create a contact, join it to 4 domain requests. 5th join is user.
|
||||
Assert that the warning on the contact form lists 4 joins."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
|
@ -4099,17 +4086,17 @@ class TestContactAdmin(TestCase):
|
|||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to User: <a href='/admin/registrar/"
|
||||
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
||||
f"user/{self.staffuser.pk}/change/'>first last staff@example.com</a></li>"
|
||||
"</ul>",
|
||||
)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_more(self):
|
||||
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
||||
"""Create a contact, join it to 5 domain requests. 6th join is user.
|
||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
# Create an instance of the model
|
||||
# join it to 5 domain requests. The 6th join will be a user.
|
||||
# join it to 6 domain requests.
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||
|
|
|
@ -1209,24 +1209,14 @@ class TestUser(TestCase):
|
|||
# test with a user with contact info defined
|
||||
self.assertTrue(self.user.has_contact_info())
|
||||
# test with a user without contact info defined
|
||||
self.user.contact.title = None
|
||||
self.user.contact.email = None
|
||||
self.user.contact.phone = None
|
||||
self.user.title = None
|
||||
self.user.email = None
|
||||
self.user.phone = None
|
||||
self.assertFalse(self.user.has_contact_info())
|
||||
|
||||
|
||||
class TestContact(TestCase):
|
||||
def setUp(self):
|
||||
self.email_for_invalid = "intern@igorville.gov"
|
||||
self.invalid_user, _ = User.objects.get_or_create(
|
||||
username=self.email_for_invalid,
|
||||
email=self.email_for_invalid,
|
||||
first_name="",
|
||||
last_name="",
|
||||
phone="",
|
||||
)
|
||||
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
|
||||
|
||||
self.email = "mayor@igorville.gov"
|
||||
self.user, _ = User.objects.get_or_create(
|
||||
email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789"
|
||||
|
@ -1242,87 +1232,6 @@ class TestContact(TestCase):
|
|||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
def test_saving_contact_updates_user_first_last_names_and_phone(self):
|
||||
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
|
||||
|
||||
# User and Contact are created and linked as expected.
|
||||
# An empty User object should create an empty contact.
|
||||
self.assertEqual(self.invalid_contact.first_name, "")
|
||||
self.assertEqual(self.invalid_contact.last_name, "")
|
||||
self.assertEqual(self.invalid_contact.phone, "")
|
||||
self.assertEqual(self.invalid_user.first_name, "")
|
||||
self.assertEqual(self.invalid_user.last_name, "")
|
||||
self.assertEqual(self.invalid_user.phone, "")
|
||||
|
||||
# Manually update the contact - mimicking production (pre-existing data)
|
||||
self.invalid_contact.first_name = "Joey"
|
||||
self.invalid_contact.last_name = "Baloney"
|
||||
self.invalid_contact.phone = "123456789"
|
||||
self.invalid_contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.invalid_user.refresh_from_db()
|
||||
|
||||
# Updating the contact's first and last names propagate to the user
|
||||
self.assertEqual(self.invalid_contact.first_name, "Joey")
|
||||
self.assertEqual(self.invalid_contact.last_name, "Baloney")
|
||||
self.assertEqual(self.invalid_contact.phone, "123456789")
|
||||
self.assertEqual(self.invalid_user.first_name, "Joey")
|
||||
self.assertEqual(self.invalid_user.last_name, "Baloney")
|
||||
self.assertEqual(self.invalid_user.phone, "123456789")
|
||||
|
||||
def test_saving_contact_does_not_update_user_first_last_names_and_phone(self):
|
||||
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
|
||||
|
||||
# User and Contact are created and linked as expected
|
||||
self.assertEqual(self.contact.first_name, "Jeff")
|
||||
self.assertEqual(self.contact.last_name, "Lebowski")
|
||||
self.assertEqual(self.contact.phone, "123456789")
|
||||
self.assertEqual(self.user.first_name, "Jeff")
|
||||
self.assertEqual(self.user.last_name, "Lebowski")
|
||||
self.assertEqual(self.user.phone, "123456789")
|
||||
|
||||
self.contact.first_name = "Joey"
|
||||
self.contact.last_name = "Baloney"
|
||||
self.contact.phone = "987654321"
|
||||
self.contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Updating the contact's first and last names propagate to the user
|
||||
self.assertEqual(self.contact.first_name, "Joey")
|
||||
self.assertEqual(self.contact.last_name, "Baloney")
|
||||
self.assertEqual(self.contact.phone, "987654321")
|
||||
self.assertEqual(self.user.first_name, "Jeff")
|
||||
self.assertEqual(self.user.last_name, "Lebowski")
|
||||
self.assertEqual(self.user.phone, "123456789")
|
||||
|
||||
def test_saving_contact_does_not_update_user_email(self):
|
||||
"""When a contact's email is updated, the change is not propagated to the user."""
|
||||
self.contact.email = "joey.baloney@diaperville.com"
|
||||
self.contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Updating the contact's email does not propagate
|
||||
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
|
||||
self.assertEqual(self.user.email, "mayor@igorville.gov")
|
||||
|
||||
def test_saving_contact_does_not_update_user_email_when_none(self):
|
||||
"""When a contact's email is updated, and the first/last name is none,
|
||||
the change is not propagated to the user."""
|
||||
self.invalid_contact.email = "joey.baloney@diaperville.com"
|
||||
self.invalid_contact.save()
|
||||
|
||||
# Refresh the user object to reflect the changes made in the database
|
||||
self.invalid_user.refresh_from_db()
|
||||
|
||||
# Updating the contact's email does not propagate
|
||||
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
|
||||
self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
|
||||
|
||||
def test_has_more_than_one_join(self):
|
||||
"""Test the Contact model method, has_more_than_one_join"""
|
||||
# test for a contact which has one user defined
|
||||
|
@ -1334,6 +1243,7 @@ class TestContact(TestCase):
|
|||
|
||||
def test_has_contact_info(self):
|
||||
"""Test that has_contact_info properly returns"""
|
||||
self.contact.title = "Title"
|
||||
# test with a contact with contact info defined
|
||||
self.assertTrue(self.contact.has_contact_info())
|
||||
# test with a contact without contact info defined
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from registrar.models import Contact
|
||||
|
||||
|
||||
class TestUserPostSave(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "test_user"
|
||||
self.first_name = "First"
|
||||
self.last_name = "Last"
|
||||
self.email = "info@example.com"
|
||||
self.phone = "202-555-0133"
|
||||
|
||||
self.preferred_first_name = "One"
|
||||
self.preferred_last_name = "Two"
|
||||
self.preferred_email = "front_desk@example.com"
|
||||
self.preferred_phone = "202-555-0134"
|
||||
|
||||
def test_user_created_without_matching_contact(self):
|
||||
"""Expect 1 Contact containing data copied from User."""
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
phone=self.phone,
|
||||
)
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
||||
|
||||
def test_user_created_with_matching_contact(self):
|
||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
Contact.objects.create(
|
||||
first_name=self.preferred_first_name,
|
||||
last_name=self.preferred_last_name,
|
||||
email=self.email, # must be the same, to find the match!
|
||||
phone=self.preferred_phone,
|
||||
)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
)
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.preferred_first_name)
|
||||
self.assertEqual(actual.last_name, self.preferred_last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.preferred_phone)
|
||||
|
||||
def test_user_updated_without_matching_contact(self):
|
||||
"""Expect 1 Contact containing data copied from User."""
|
||||
# create the user
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(username=self.username, first_name="", last_name="", email="", phone="")
|
||||
# delete the contact
|
||||
Contact.objects.all().delete()
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
# modify the user
|
||||
user.username = self.username
|
||||
user.first_name = self.first_name
|
||||
user.last_name = self.last_name
|
||||
user.email = self.email
|
||||
user.phone = self.phone
|
||||
user.save()
|
||||
# test
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
||||
|
||||
def test_user_updated_with_matching_contact(self):
|
||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
||||
# create the user
|
||||
self.assertEqual(len(Contact.objects.all()), 0)
|
||||
user = get_user_model().objects.create(
|
||||
username=self.username,
|
||||
first_name=self.first_name,
|
||||
last_name=self.last_name,
|
||||
email=self.email,
|
||||
phone=self.phone,
|
||||
)
|
||||
# modify the user
|
||||
user.first_name = self.preferred_first_name
|
||||
user.last_name = self.preferred_last_name
|
||||
user.email = self.preferred_email
|
||||
user.phone = self.preferred_phone
|
||||
user.save()
|
||||
# test
|
||||
actual = Contact.objects.get(user=user)
|
||||
self.assertEqual(actual.first_name, self.first_name)
|
||||
self.assertEqual(actual.last_name, self.last_name)
|
||||
self.assertEqual(actual.email, self.email)
|
||||
self.assertEqual(actual.phone, self.phone)
|
|
@ -57,12 +57,10 @@ class TestWithUser(MockEppLib):
|
|||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
phone = "8003111234"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||
)
|
||||
title = "test title"
|
||||
self.user.contact.title = title
|
||||
self.user.contact.save()
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, title=title, email=email, phone=phone
|
||||
)
|
||||
|
||||
username_regular_incomplete = "test_regular_user_incomplete"
|
||||
username_other_incomplete = "test_other_user_incomplete"
|
||||
|
@ -560,7 +558,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
|
@ -598,7 +596,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
|
@ -613,7 +611,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
|||
|
||||
# Submit the form using the specific submit button to execute the redirect
|
||||
completed_setup_page = self._submit_form_webtest(
|
||||
finish_setup_form, follow=True, name="contact_setup_submit_button"
|
||||
finish_setup_form, follow=True, name="user_setup_submit_button"
|
||||
)
|
||||
self.assertEqual(completed_setup_page.status_code, 200)
|
||||
|
||||
|
|
|
@ -1478,8 +1478,8 @@ class TestDomainContactInformation(TestDomainOverview):
|
|||
|
||||
def test_domain_your_contact_information_content(self):
|
||||
"""Logged-in user's contact information appears on the page."""
|
||||
self.user.contact.first_name = "Testy"
|
||||
self.user.contact.save()
|
||||
self.user.first_name = "Testy"
|
||||
self.user.save()
|
||||
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ from registrar.models.utility.contact_error import ContactError
|
|||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
UserForm,
|
||||
AuthorizingOfficialContactForm,
|
||||
DomainOrgNameAddressForm,
|
||||
DomainAddUserForm,
|
||||
|
@ -573,7 +573,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
|||
"""Domain your contact information editing view."""
|
||||
|
||||
template_name = "domain_your_contact_information.html"
|
||||
form_class = ContactForm
|
||||
form_class = UserForm
|
||||
|
||||
@waffle_flag("!profile_feature") # type: ignore
|
||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||
|
@ -582,7 +582,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
|||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.submitter instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.request.user.contact
|
||||
form_kwargs["instance"] = self.request.user
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
|
|
|
@ -9,9 +9,6 @@ from django.http import QueryDict
|
|||
from django.views.generic.edit import FormMixin
|
||||
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||
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
|
||||
|
@ -25,7 +22,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
Base View for the User Profile. Handles getting and setting the User Profile
|
||||
"""
|
||||
|
||||
model = Contact
|
||||
model = User
|
||||
template_name = "profile.html"
|
||||
form_class = UserProfileForm
|
||||
base_view_name = "user-profile"
|
||||
|
@ -57,6 +54,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
def get_context_data(self, **kwargs):
|
||||
"""Extend get_context_data to include has_profile_feature_flag"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
logger.info("UserProfileView::get_context_data")
|
||||
# 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")
|
||||
|
||||
|
@ -123,9 +121,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
def get_object(self, queryset=None):
|
||||
"""Override get_object to return the logged-in user's 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
|
||||
return self.user
|
||||
|
||||
|
||||
class FinishProfileSetupView(UserProfileView):
|
||||
|
@ -134,7 +130,7 @@ class FinishProfileSetupView(UserProfileView):
|
|||
|
||||
template_name = "finish_profile_setup.html"
|
||||
form_class = FinishSetupProfileForm
|
||||
model = Contact
|
||||
model = User
|
||||
|
||||
base_view_name = "finish-user-profile-setup"
|
||||
|
||||
|
@ -160,11 +156,11 @@ class FinishProfileSetupView(UserProfileView):
|
|||
# Get the current form and validate it
|
||||
if form.is_valid():
|
||||
self.redirect_page = False
|
||||
if "contact_setup_save_button" in request.POST:
|
||||
if "user_setup_save_button" in request.POST:
|
||||
# Logic for when the 'Save' button is clicked, which indicates
|
||||
# user should stay on this page
|
||||
self.redirect_page = False
|
||||
elif "contact_setup_submit_button" in request.POST:
|
||||
elif "user_setup_submit_button" in request.POST:
|
||||
# Logic for when the other button is clicked, which indicates
|
||||
# the user should be taken to the redirect page
|
||||
self.redirect_page = True
|
||||
|
|
|
@ -4,7 +4,7 @@ import abc # abstract base class
|
|||
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||
from registrar.models import Domain, DomainRequest, DomainInvitation
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
from .mixins import (
|
||||
|
@ -154,9 +154,9 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
|
|||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = Contact
|
||||
model = User
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "contact"
|
||||
context_object_name = "user"
|
||||
|
||||
# Abstract property enforces NotImplementedError on an attribute.
|
||||
@property
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue