diff --git a/bats_ai/core/admin/__init__.py b/bats_ai/core/admin/__init__.py index db0b24a8..467ad34e 100644 --- a/bats_ai/core/admin/__init__.py +++ b/bats_ai/core/admin/__init__.py @@ -18,6 +18,7 @@ from .species import SpeciesAdmin from .spectrogram import SpectrogramAdmin from .spectrogram_image import SpectrogramImageAdmin +from .user import UserAdmin from .vetting_details import VettingDetailsAdmin __all__ = [ @@ -36,6 +37,7 @@ 'SpectrogramImageAdmin', 'VettingDetailsAdmin', 'PulseMetadataAdmin', + 'UserAdmin', # NABat Models 'NABatRecordingAnnotationAdmin', 'NABatCompressedSpectrogramAdmin', diff --git a/bats_ai/core/admin/user.py b/bats_ai/core/admin/user.py new file mode 100644 index 00000000..2c7dc14d --- /dev/null +++ b/bats_ai/core/admin/user.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.contrib.auth.models import User + +from bats_ai.core.models import UserProfile + + +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + extra = 0 + + +admin.site.unregister(User) + + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + inlines = [UserProfileInline] + list_select_related = ['profile'] + + # See https://code.djangoproject.com/ticket/36926#ticket + list_display = list(BaseUserAdmin.list_display) + ['is_verified'] + list_filter = list(BaseUserAdmin.list_filter) + ['profile__verified'] + + @admin.display( + boolean=True, + description='Is Verified?', + ) + def is_verified(self, obj): + return obj.profile.verified diff --git a/bats_ai/core/migrations/0030_userprofile.py b/bats_ai/core/migrations/0030_userprofile.py new file mode 100644 index 00000000..2627affd --- /dev/null +++ b/bats_ai/core/migrations/0030_userprofile.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.11 on 2026-02-12 20:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def create_user_profiles(apps, schema_editor): + User = apps.get_model('auth', 'User') + UserProfile = apps.get_model('core', 'UserProfile') + + for user in User.objects.all(): + UserProfile.objects.create(user=user) + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0029_pulsemetadata_char_freq_pulsemetadata_curve_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ('verified', models.BooleanField(default=False)), + ( + 'user', + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='profile', + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.RunPython(create_user_profiles, reverse_code=migrations.RunPython.noop), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index 3c254170..6e777800 100644 --- a/bats_ai/core/models/__init__.py +++ b/bats_ai/core/models/__init__.py @@ -12,6 +12,7 @@ from .species import Species from .spectrogram import Spectrogram from .spectrogram_image import SpectrogramImage +from .user_profile import UserProfile from .vetting_details import VettingDetails __all__ = [ @@ -31,5 +32,6 @@ 'ProcessingTaskType', 'ExportedAnnotationFile', 'SpectrogramImage', + 'UserProfile', 'VettingDetails', ] diff --git a/bats_ai/core/models/user_profile.py b/bats_ai/core/models/user_profile.py new file mode 100644 index 00000000..ee5e73b2 --- /dev/null +++ b/bats_ai/core/models/user_profile.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import User +from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver + + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + verified = models.BooleanField(default=False) + + +@receiver(post_save, sender=User, dispatch_uid='create_new_user_profile') +def _create_new_user_profile(sender, instance, created, **kwargs): + if created: + UserProfile.objects.create(user=instance) diff --git a/bats_ai/core/tests/factories.py b/bats_ai/core/tests/factories.py index fceedd67..dbd974fa 100644 --- a/bats_ai/core/tests/factories.py +++ b/bats_ai/core/tests/factories.py @@ -1,24 +1,41 @@ from django.contrib.auth.models import User +from django.db.models.signals import post_save import factory.django -from bats_ai.core.models import VettingDetails +from bats_ai.core.models import UserProfile, VettingDetails +@factory.django.mute_signals(post_save) class UserFactory(factory.django.DjangoModelFactory[User]): class Meta: model = User + skip_postgeneration_save = True username = factory.SelfAttribute('email') email = factory.Faker('safe_email') first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') + profile = factory.RelatedFactory( + 'bats_ai.core.tests.factories.UserProfileFactory', factory_related_name='user' + ) + class SuperuserFactory(UserFactory): is_superuser = True is_staff = True +@factory.django.mute_signals(post_save) +class UserProfileFactory(factory.django.DjangoModelFactory[UserProfile]): + class Meta: + model = UserProfile + skip_postgeneration_save = True + + verified = True + user = factory.SubFactory(UserFactory, profile=None) + + class VettingDetailsFactory(factory.django.DjangoModelFactory[VettingDetails]): class Meta: diff --git a/bats_ai/core/tests/test_user_profile.py b/bats_ai/core/tests/test_user_profile.py new file mode 100644 index 00000000..7f6e9140 --- /dev/null +++ b/bats_ai/core/tests/test_user_profile.py @@ -0,0 +1,13 @@ +from django.contrib.auth.models import User +import pytest + +from bats_ai.core.models import UserProfile + + +@pytest.mark.django_db +def test_profile_creation(): + # Use django model directly to test the signal receiver, + # not whether our factories are working as intended. + user = User.objects.create() + profile = UserProfile.objects.get(user=user) + assert not profile.verified