♻️(contacts) move user profile to contact
Move the user <-> contact relation for "profile" to the contact model. Now the Contact model is the only one to point to User (and not backward). Contact: - FK to User for the owner - FK to User for the profile
This commit is contained in:
@@ -12,6 +12,10 @@ and this project adheres to
|
|||||||
|
|
||||||
- ✨(contacts) add notes & force full_name #565
|
- ✨(contacts) add notes & force full_name #565
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♻️(contacts) move user profile to contact #572
|
||||||
|
|
||||||
## [1.7.1] - 2024-11-28
|
## [1.7.1] - 2024-11-28
|
||||||
|
|
||||||
## [1.7.0] - 2024-11-28
|
## [1.7.0] - 2024-11-28
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ from mozilla_django_oidc.auth import (
|
|||||||
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
|
||||||
)
|
)
|
||||||
|
|
||||||
from core.models import Organization, OrganizationAccess, OrganizationRoleChoices
|
from core.models import (
|
||||||
|
Contact,
|
||||||
|
Organization,
|
||||||
|
OrganizationAccess,
|
||||||
|
OrganizationRoleChoices,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -196,6 +201,19 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
user=user,
|
user=user,
|
||||||
role=OrganizationRoleChoices.ADMIN,
|
role=OrganizationRoleChoices.ADMIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Initiate the user's profile
|
||||||
|
Contact.objects.create(
|
||||||
|
owner=user,
|
||||||
|
user=user,
|
||||||
|
full_name=name or email,
|
||||||
|
data={
|
||||||
|
"emails": [
|
||||||
|
{"type": "Work", "value": email},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def compute_full_name(self, user_info):
|
def compute_full_name(self, user_info):
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class ContactFactory(BaseContactFactory):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
owner = factory.SubFactory("core.factories.UserFactory", profile_contact=None)
|
owner = factory.SubFactory("core.factories.UserFactory")
|
||||||
|
|
||||||
|
|
||||||
class OverrideContactFactory(BaseContactFactory):
|
class OverrideContactFactory(BaseContactFactory):
|
||||||
@@ -125,7 +125,7 @@ class OverrideContactFactory(BaseContactFactory):
|
|||||||
model = models.Contact
|
model = models.Contact
|
||||||
|
|
||||||
override = factory.SubFactory("core.factories.ContactFactory", owner=None)
|
override = factory.SubFactory("core.factories.ContactFactory", owner=None)
|
||||||
owner = factory.SubFactory("core.factories.UserFactory", profile_contact=None)
|
owner = factory.SubFactory("core.factories.UserFactory")
|
||||||
|
|
||||||
|
|
||||||
class OrganizationFactory(factory.django.DjangoModelFactory):
|
class OrganizationFactory(factory.django.DjangoModelFactory):
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2024-12-02 10:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def init_profile_contact(apps, schema_editor):
|
||||||
|
"""Create a Contact for each User to be used as their profile."""
|
||||||
|
User = apps.get_model("core", "User")
|
||||||
|
Contact = apps.get_model("core", "Contact")
|
||||||
|
|
||||||
|
for user in User.objects.all(): # This is a small table for now
|
||||||
|
Contact.objects.create(
|
||||||
|
owner=user,
|
||||||
|
user=user,
|
||||||
|
full_name=user.name or user.email,
|
||||||
|
data={
|
||||||
|
"emails": [
|
||||||
|
{"type": "Work", "value": user.email},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0007_rename_contact_base_to_override'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='profile_contact',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contact',
|
||||||
|
name='user',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='profile_contact', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='contact',
|
||||||
|
constraint=models.CheckConstraint(condition=models.Q(('user__isnull', True), models.Q(('owner', models.F('user')), ('owner__isnull', False)), _connector='OR'), name='profile_contact_owner_constraint', violation_error_message='Users can only declare as profile a contact they own.'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(init_profile_contact, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -113,6 +113,14 @@ class Contact(BaseModel):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
user = models.OneToOneField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="profile_contact",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
full_name = models.CharField(_("full name"), max_length=150)
|
full_name = models.CharField(_("full name"), max_length=150)
|
||||||
short_name = models.CharField(_("short name"), max_length=30, null=True, blank=True)
|
short_name = models.CharField(_("short name"), max_length=30, null=True, blank=True)
|
||||||
|
|
||||||
@@ -148,6 +156,13 @@ class Contact(BaseModel):
|
|||||||
name="override_not_self",
|
name="override_not_self",
|
||||||
violation_error_message="A contact cannot override itself.",
|
violation_error_message="A contact cannot override itself.",
|
||||||
),
|
),
|
||||||
|
# When a user is set, the owner must be the user
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(user__isnull=True)
|
||||||
|
| models.Q(owner=models.F("user"), owner__isnull=False),
|
||||||
|
name="profile_contact_owner_constraint",
|
||||||
|
violation_error_message="Users can only declare as profile a contact they own.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -392,13 +407,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
)
|
)
|
||||||
email = models.EmailField(_("email address"), null=True, blank=True)
|
email = models.EmailField(_("email address"), null=True, blank=True)
|
||||||
name = models.CharField(_("name"), max_length=100, null=True, blank=True)
|
name = models.CharField(_("name"), max_length=100, null=True, blank=True)
|
||||||
profile_contact = models.OneToOneField(
|
|
||||||
Contact,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name="user",
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
max_length=10,
|
max_length=10,
|
||||||
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
|
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
|
||||||
@@ -467,11 +475,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
if self.email:
|
if self.email:
|
||||||
self.email = User.objects.normalize_email(self.email)
|
self.email = User.objects.normalize_email(self.email)
|
||||||
|
|
||||||
if self.profile_contact_id and not self.profile_contact.owner == self:
|
|
||||||
raise exceptions.ValidationError(
|
|
||||||
"Users can only declare as profile a contact they own."
|
|
||||||
)
|
|
||||||
|
|
||||||
def _convert_valid_invitations(self):
|
def _convert_valid_invitations(self):
|
||||||
"""
|
"""
|
||||||
Convert valid invitations to team accesses.
|
Convert valid invitations to team accesses.
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ def test_api_contacts_list_authenticated_no_query():
|
|||||||
Profile and overridden contacts should be excluded.
|
Profile and overridden contacts should be excluded.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
contact = factories.ContactFactory(owner=user)
|
factories.ContactFactory(owner=user, user=user)
|
||||||
user.profile_contact = contact
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
# Let's have 5 contacts in database:
|
# Let's have 5 contacts in database:
|
||||||
assert user.profile_contact is not None # Excluded because profile contact
|
assert user.profile_contact is not None # Excluded because profile contact
|
||||||
@@ -332,7 +330,7 @@ def test_api_contacts_create_anonymous_forbidden():
|
|||||||
|
|
||||||
def test_api_contacts_create_authenticated_missing_base():
|
def test_api_contacts_create_authenticated_missing_base():
|
||||||
"""Authenticated user should be able to create contact without override."""
|
"""Authenticated user should be able to create contact without override."""
|
||||||
user = factories.UserFactory(profile_contact=None)
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
@@ -363,7 +361,7 @@ def test_api_contacts_create_authenticated_missing_base():
|
|||||||
|
|
||||||
def test_api_contacts_create_authenticated_successful():
|
def test_api_contacts_create_authenticated_successful():
|
||||||
"""Authenticated users should be able to create contacts."""
|
"""Authenticated users should be able to create contacts."""
|
||||||
user = factories.UserFactory(profile_contact=None)
|
user = factories.UserFactory()
|
||||||
base_contact = factories.BaseContactFactory()
|
base_contact = factories.BaseContactFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
@@ -410,7 +408,7 @@ def test_api_contacts_create_authenticated_existing_override():
|
|||||||
Trying to create a contact overriding a contact that is already overridden by the user
|
Trying to create a contact overriding a contact that is already overridden by the user
|
||||||
should receive a 400 error.
|
should receive a 400 error.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory(profile_contact=None)
|
user = factories.UserFactory()
|
||||||
base_contact = factories.BaseContactFactory()
|
base_contact = factories.BaseContactFactory()
|
||||||
factories.ContactFactory(override=base_contact, owner=user)
|
factories.ContactFactory(override=base_contact, owner=user)
|
||||||
|
|
||||||
@@ -439,7 +437,7 @@ def test_api_contacts_create_authenticated_existing_override():
|
|||||||
|
|
||||||
def test_api_contacts_create_authenticated_successful_with_notes():
|
def test_api_contacts_create_authenticated_successful_with_notes():
|
||||||
"""Authenticated users should be able to create contacts with notes."""
|
"""Authenticated users should be able to create contacts with notes."""
|
||||||
user = factories.UserFactory(profile_contact=None)
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
@@ -504,7 +502,7 @@ def test_api_contacts_update_authenticated_owned():
|
|||||||
"""
|
"""
|
||||||
Authenticated users should be allowed to update their own contacts.
|
Authenticated users should be allowed to update their own contacts.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory(profile_contact=None)
|
user = factories.UserFactory()
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
@@ -543,9 +541,7 @@ def test_api_contacts_update_authenticated_profile():
|
|||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
contact = factories.ContactFactory(owner=user)
|
contact = factories.ContactFactory(owner=user, user=user)
|
||||||
user.profile_contact = contact
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
old_contact_values = serializers.ContactSerializer(instance=contact).data
|
old_contact_values = serializers.ContactSerializer(instance=contact).data
|
||||||
new_contact_values = serializers.ContactSerializer(
|
new_contact_values = serializers.ContactSerializer(
|
||||||
@@ -678,9 +674,7 @@ def test_api_contacts_delete_authenticated_profile():
|
|||||||
Authenticated users should be allowed to delete their profile contact.
|
Authenticated users should be allowed to delete their profile contact.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
contact = factories.ContactFactory(owner=user, override=None)
|
contact = factories.ContactFactory(owner=user, user=user)
|
||||||
user.profile_contact = contact
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|||||||
@@ -534,9 +534,7 @@ def test_api_users_retrieve_me_authenticated():
|
|||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
# Define profile contact
|
# Define profile contact
|
||||||
contact = factories.ContactFactory(owner=user)
|
factories.ContactFactory(owner=user, user=user)
|
||||||
user.profile_contact = contact
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
factories.UserFactory.create_batch(2)
|
factories.UserFactory.create_batch(2)
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -575,9 +573,7 @@ def test_api_users_retrieve_me_authenticated_abilities():
|
|||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
|
||||||
# Define profile contact
|
# Define profile contact
|
||||||
contact = factories.ContactFactory(owner=user)
|
factories.ContactFactory(owner=user, user=user)
|
||||||
user.profile_contact = contact
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
factories.UserFactory.create_batch(2)
|
factories.UserFactory.create_batch(2)
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,8 @@ def test_models_contacts_base_not_owned():
|
|||||||
|
|
||||||
def test_models_contacts_profile_not_owned():
|
def test_models_contacts_profile_not_owned():
|
||||||
"""A contact cannot be defined as profile for a user if is not owned."""
|
"""A contact cannot be defined as profile for a user if is not owned."""
|
||||||
base_contact = factories.ContactFactory(owner=None)
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as excinfo:
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
factories.UserFactory(profile_contact=base_contact)
|
factories.ContactFactory(owner=None, user=factories.UserFactory())
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
str(excinfo.value)
|
str(excinfo.value)
|
||||||
@@ -88,7 +86,8 @@ def test_models_contacts_profile_owned_by_other():
|
|||||||
contact = factories.ContactFactory()
|
contact = factories.ContactFactory()
|
||||||
|
|
||||||
with pytest.raises(ValidationError) as excinfo:
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
factories.UserFactory(profile_contact=contact)
|
contact.user = factories.UserFactory()
|
||||||
|
contact.save()
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
str(excinfo.value)
|
str(excinfo.value)
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ def test_models_users_profile_not_owned():
|
|||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
contact = factories.ContactFactory(override=None, owner=None)
|
contact = factories.ContactFactory(override=None, owner=None)
|
||||||
|
|
||||||
user.profile_contact = contact
|
|
||||||
with pytest.raises(ValidationError) as excinfo:
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
user.save()
|
contact.user = user
|
||||||
|
contact.save()
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
str(excinfo.value)
|
str(excinfo.value)
|
||||||
@@ -150,9 +150,9 @@ def test_models_users_profile_owned_by_other():
|
|||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
contact = factories.ContactFactory()
|
contact = factories.ContactFactory()
|
||||||
|
|
||||||
user.profile_contact = contact
|
|
||||||
with pytest.raises(ValidationError) as excinfo:
|
with pytest.raises(ValidationError) as excinfo:
|
||||||
user.save()
|
contact.user = user
|
||||||
|
contact.save()
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
str(excinfo.value)
|
str(excinfo.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user