Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bats_ai/core/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -36,6 +37,7 @@
'SpectrogramImageAdmin',
'VettingDetailsAdmin',
'PulseMetadataAdmin',
'UserAdmin',
# NABat Models
'NABatRecordingAnnotationAdmin',
'NABatCompressedSpectrogramAdmin',
Expand Down
31 changes: 31 additions & 0 deletions bats_ai/core/admin/user.py
Original file line number Diff line number Diff line change
@@ -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']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing profile__verified doesn't work?

list_display supports this:

The name of a related field, using the __ notation. For example:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked but I couldn't figure out how to display the checkmark/X icons. The value in the column would just be the string "True"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a bug with Django. Can you file a bug report: https://code.djangoproject.com/query

Obviously, it's fine to keep the extra code for now then. Could you add a code comment with a link to the bug?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_filter = list(BaseUserAdmin.list_filter) + ['profile__verified']

@admin.display(
boolean=True,
description='Is Verified?',
)
def is_verified(self, obj):
return obj.profile.verified
45 changes: 45 additions & 0 deletions bats_ai/core/migrations/0030_userprofile.py
Original file line number Diff line number Diff line change
@@ -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),
]
2 changes: 2 additions & 0 deletions bats_ai/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -31,5 +32,6 @@
'ProcessingTaskType',
'ExportedAnnotationFile',
'SpectrogramImage',
'UserProfile',
'VettingDetails',
]
15 changes: 15 additions & 0 deletions bats_ai/core/models/user_profile.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 18 additions & 1 deletion bats_ai/core/tests/factories.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
13 changes: 13 additions & 0 deletions bats_ai/core/tests/test_user_profile.py
Original file line number Diff line number Diff line change
@@ -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