Skip to content

Commit a30b731

Browse files
authored
Create user profile model (#359)
* Create user profile model * Address factoryboy deprecation warning * Add smell test for user profile creation * Make profile creation receiver private * Clarify tests and factories * Add user profiles to user admin * Use `get` over `filter` for testing profile create * Add verified filter to user admin * Use the register decorator * Use double underscore for list filter * Add data migration to create user profiles * Remove hasattr check for profile in user admin * Remove unnecessary post gen hook * Squash migrations * Link to ticket explaining `is_verified` * Add trailing comma
1 parent 59275a8 commit a30b731

File tree

7 files changed

+126
-1
lines changed

7 files changed

+126
-1
lines changed

bats_ai/core/admin/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .species import SpeciesAdmin
1919
from .spectrogram import SpectrogramAdmin
2020
from .spectrogram_image import SpectrogramImageAdmin
21+
from .user import UserAdmin
2122
from .vetting_details import VettingDetailsAdmin
2223

2324
__all__ = [
@@ -36,6 +37,7 @@
3637
'SpectrogramImageAdmin',
3738
'VettingDetailsAdmin',
3839
'PulseMetadataAdmin',
40+
'UserAdmin',
3941
# NABat Models
4042
'NABatRecordingAnnotationAdmin',
4143
'NABatCompressedSpectrogramAdmin',

bats_ai/core/admin/user.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from django.contrib import admin
2+
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
3+
from django.contrib.auth.models import User
4+
5+
from bats_ai.core.models import UserProfile
6+
7+
8+
class UserProfileInline(admin.StackedInline):
9+
model = UserProfile
10+
can_delete = False
11+
extra = 0
12+
13+
14+
admin.site.unregister(User)
15+
16+
17+
@admin.register(User)
18+
class UserAdmin(BaseUserAdmin):
19+
inlines = [UserProfileInline]
20+
list_select_related = ['profile']
21+
22+
# See https://code.djangoproject.com/ticket/36926#ticket
23+
list_display = list(BaseUserAdmin.list_display) + ['is_verified']
24+
list_filter = list(BaseUserAdmin.list_filter) + ['profile__verified']
25+
26+
@admin.display(
27+
boolean=True,
28+
description='Is Verified?',
29+
)
30+
def is_verified(self, obj):
31+
return obj.profile.verified
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 5.2.11 on 2026-02-12 20:10
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
def create_user_profiles(apps, schema_editor):
9+
User = apps.get_model('auth', 'User')
10+
UserProfile = apps.get_model('core', 'UserProfile')
11+
12+
for user in User.objects.all():
13+
UserProfile.objects.create(user=user)
14+
15+
16+
class Migration(migrations.Migration):
17+
18+
dependencies = [
19+
('core', '0029_pulsemetadata_char_freq_pulsemetadata_curve_and_more'),
20+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
21+
]
22+
23+
operations = [
24+
migrations.CreateModel(
25+
name='UserProfile',
26+
fields=[
27+
(
28+
'id',
29+
models.BigAutoField(
30+
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
31+
),
32+
),
33+
('verified', models.BooleanField(default=False)),
34+
(
35+
'user',
36+
models.OneToOneField(
37+
on_delete=django.db.models.deletion.CASCADE,
38+
related_name='profile',
39+
to=settings.AUTH_USER_MODEL,
40+
),
41+
),
42+
],
43+
),
44+
migrations.RunPython(create_user_profiles, reverse_code=migrations.RunPython.noop),
45+
]

bats_ai/core/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .species import Species
1313
from .spectrogram import Spectrogram
1414
from .spectrogram_image import SpectrogramImage
15+
from .user_profile import UserProfile
1516
from .vetting_details import VettingDetails
1617

1718
__all__ = [
@@ -31,5 +32,6 @@
3132
'ProcessingTaskType',
3233
'ExportedAnnotationFile',
3334
'SpectrogramImage',
35+
'UserProfile',
3436
'VettingDetails',
3537
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from django.contrib.auth.models import User
2+
from django.db import models
3+
from django.db.models.signals import post_save
4+
from django.dispatch import receiver
5+
6+
7+
class UserProfile(models.Model):
8+
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
9+
verified = models.BooleanField(default=False)
10+
11+
12+
@receiver(post_save, sender=User, dispatch_uid='create_new_user_profile')
13+
def _create_new_user_profile(sender, instance, created, **kwargs):
14+
if created:
15+
UserProfile.objects.create(user=instance)

bats_ai/core/tests/factories.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
from django.contrib.auth.models import User
2+
from django.db.models.signals import post_save
23
import factory.django
34

4-
from bats_ai.core.models import VettingDetails
5+
from bats_ai.core.models import UserProfile, VettingDetails
56

67

8+
@factory.django.mute_signals(post_save)
79
class UserFactory(factory.django.DjangoModelFactory[User]):
810
class Meta:
911
model = User
12+
skip_postgeneration_save = True
1013

1114
username = factory.SelfAttribute('email')
1215
email = factory.Faker('safe_email')
1316
first_name = factory.Faker('first_name')
1417
last_name = factory.Faker('last_name')
1518

19+
profile = factory.RelatedFactory(
20+
'bats_ai.core.tests.factories.UserProfileFactory', factory_related_name='user'
21+
)
22+
1623

1724
class SuperuserFactory(UserFactory):
1825
is_superuser = True
1926
is_staff = True
2027

2128

29+
@factory.django.mute_signals(post_save)
30+
class UserProfileFactory(factory.django.DjangoModelFactory[UserProfile]):
31+
class Meta:
32+
model = UserProfile
33+
skip_postgeneration_save = True
34+
35+
verified = True
36+
user = factory.SubFactory(UserFactory, profile=None)
37+
38+
2239
class VettingDetailsFactory(factory.django.DjangoModelFactory[VettingDetails]):
2340

2441
class Meta:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.contrib.auth.models import User
2+
import pytest
3+
4+
from bats_ai.core.models import UserProfile
5+
6+
7+
@pytest.mark.django_db
8+
def test_profile_creation():
9+
# Use django model directly to test the signal receiver,
10+
# not whether our factories are working as intended.
11+
user = User.objects.create()
12+
profile = UserProfile.objects.get(user=user)
13+
assert not profile.verified

0 commit comments

Comments
 (0)