Skip to content
Open
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/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
RecordingRouter,
RecordingTagRouter,
SpeciesRouter,
VettingRouter,
)
from bats_ai.core.views.nabat import NABatConfigurationRouter, NABatRecordingRouter

Expand Down Expand Up @@ -46,3 +47,4 @@ def global_auth(request):
api.add_router('/recording-tag/', RecordingTagRouter)
api.add_router('/nabat/recording/', NABatRecordingRouter)
api.add_router('/nabat/configuration/', NABatConfigurationRouter)
api.add_router('/vetting/', VettingRouter)
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 .vetting_details import VettingDetailsAdmin

__all__ = [
'AnnotationsAdmin',
Expand All @@ -34,6 +35,7 @@
'ConfigurationAdmin',
'ExportedAnnotationFileAdmin',
'SpectrogramImageAdmin',
'VettingDetailsAdmin',
# NABat Models
'NABatRecordingAnnotationAdmin',
'NABatCompressedSpectrogramAdmin',
Expand Down
13 changes: 13 additions & 0 deletions bats_ai/core/admin/vetting_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.contrib import admin

from bats_ai.core.models import VettingDetails


@admin.register(VettingDetails)
class VettingDetailsAdmin(admin.ModelAdmin):
list_display = [
'pk',
'user',
# 'reference_materials',
]
search_fields = ('user',)
41 changes: 41 additions & 0 deletions bats_ai/core/migrations/0025_vettingdetails_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 4.2.23 on 2026-01-08 18:57

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0024_configuration_mark_annotations_completed_enabled_and_more'),
]

operations = [
migrations.CreateModel(
name='VettingDetails',
fields=[
(
'id',
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
('reference_materials', models.TextField(blank=True)),
(
'user',
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddConstraint(
model_name='vettingdetails',
constraint=models.CheckConstraint(
check=models.Q(('reference_materials__length__lte', 2000)),
name='reference_materials_max_2000',
),
),
]
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 .vetting_details import VettingDetails

__all__ = [
'Annotations',
Expand All @@ -30,4 +31,5 @@
'ProcessingTaskType',
'ExportedAnnotationFile',
'SpectrogramImage',
'VettingDetails',
]
20 changes: 20 additions & 0 deletions bats_ai/core/models/vetting_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from django.contrib.auth.models import User
from django.db import models
from django.db.models import Q
from django.db.models.functions import Length

models.TextField.register_lookup(Length, 'length')


class VettingDetails(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
reference_materials = models.TextField(blank=True)

class Meta:
constraints = [
models.CheckConstraint(
# TODO change to 'condition' in Django v6
check=Q(reference_materials__length__lte=2000),
name='reference_materials_max_2000',
)
]
14 changes: 13 additions & 1 deletion bats_ai/core/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from django.test import Client
import pytest

from .factories import SuperuserFactory, UserFactory
from bats_ai.core.models import VettingDetails

from .factories import SuperuserFactory, UserFactory, VettingDetailsFactory


@pytest.fixture
Expand Down Expand Up @@ -32,3 +34,13 @@ def authorized_client(superuser: User) -> Client:
client = Client()
client.force_login(user=superuser)
return client


@pytest.fixture
def vetting_details(user: User) -> VettingDetails:
return VettingDetailsFactory(user=user)


@pytest.fixture
def random_user_vetting_details() -> VettingDetails:
return VettingDetailsFactory(user=UserFactory())
11 changes: 11 additions & 0 deletions bats_ai/core/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.contrib.auth.models import User
import factory.django

from bats_ai.core.models import VettingDetails


class UserFactory(factory.django.DjangoModelFactory[User]):
class Meta:
Expand Down Expand Up @@ -28,3 +30,12 @@ class Meta:
def _create(cls, model_class, *args, **kwargs):
manager = cls._get_manager(model_class)
return manager.create_superuser(*args, **kwargs)


class VettingDetailsFactory(factory.django.DjangoModelFactory[VettingDetails]):

class Meta:
model = VettingDetails

user = factory.SubFactory(UserFactory)
reference_materials = factory.Faker('paragraph', nb_sentences=3)
103 changes: 103 additions & 0 deletions bats_ai/core/tests/test_vetting_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

from .factories import UserFactory, VettingDetailsFactory


@pytest.mark.parametrize(
'client_fixture,status_code',
[
('client', 401),
('authenticated_client', 200),
('authorized_client', 200),
],
)
@pytest.mark.django_db
def test_get_vetting_details(client_fixture, status_code, user, vetting_details, request):
api_client = request.getfixturevalue(client_fixture)
resp = api_client.get(f'/api/v1/vetting/user/{user.id}')
assert resp.status_code == status_code
if status_code == 200:
assert resp.json()['reference_materials'] == vetting_details.reference_materials


@pytest.mark.django_db
def test_get_vetting_details_other_user(authenticated_client):
other_user = UserFactory()
VettingDetailsFactory(user=other_user)
resp = authenticated_client.get(f'/api/v1/vetting/user/{other_user.id}')
assert resp.status_code == 404


@pytest.mark.django_db
def test_create_vetting_details(client):
test_text = 'foo'
data = {'reference_materials': test_text}
test_user = UserFactory()
client.force_login(user=test_user)
resp = client.post(
f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json'
)
assert resp.status_code == 200
assert resp.json()['user_id'] == test_user.id


@pytest.mark.parametrize(
'client_fixture,status_code',
[
('authenticated_client', 404),
('authorized_client', 200),
],
)
@pytest.mark.django_db
def test_create_vetting_details_other_user(client_fixture, status_code, request):
api_client = request.getfixturevalue(client_fixture)
test_text = 'foo'
data = {'reference_materials': test_text}
other_user = UserFactory()
resp = api_client.post(
f'/api/v1/vetting/user/{other_user.id}', data=data, content_type='application/json'
)
assert resp.status_code == status_code
if status_code == 200:
assert resp.json()['reference_materials'] == test_text


@pytest.mark.django_db
def test_update_vetting_details(client):
test_text = 'bar'
data = {'reference_materials': 'bar'}
test_user = UserFactory()
VettingDetailsFactory(user=test_user, reference_materials='foo')
client.force_login(test_user)

initial_resp = client.get(f'/api/v1/vetting/user/{test_user.id}')
assert initial_resp.status_code == 200

resp = client.post(
f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json'
)
assert resp.status_code == 200

new_details_response = client.get(f'/api/v1/vetting/user/{test_user.id}')
assert new_details_response.status_code == 200
assert new_details_response.json()['reference_materials'] == test_text


@pytest.mark.parametrize(
'client_fixture,status_code',
[
('authenticated_client', 404),
('authorized_client', 200),
],
)
@pytest.mark.django_db
def test_update_vetting_details_other_user(
client_fixture, status_code, random_user_vetting_details, request
):
api_client = request.getfixturevalue(client_fixture)
resp = api_client.post(
f'/api/v1/vetting/user/{random_user_vetting_details.user.id}',
data={'reference_materials': 'foo'},
content_type='application/json',
)
assert resp.status_code == status_code
2 changes: 2 additions & 0 deletions bats_ai/core/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .recording_tag import router as RecordingTagRouter
from .sequence_annotations import router as SequenceAnnotationRouter
from .species import router as SpeciesRouter
from .vetting_details import router as VettingRouter

__all__ = [
'RecordingRouter',
Expand All @@ -22,4 +23,5 @@
'ProcessingTaskRouter',
'ExportAnnotationRouter',
'RecordingTagRouter',
'VettingRouter',
]
1 change: 1 addition & 0 deletions bats_ai/core/views/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ def get_current_user(request):
return {
'email': request.user.email,
'name': request.user.username,
'id': request.user.id,
}
return {'email': '', 'name': ''}
57 changes: 57 additions & 0 deletions bats_ai/core/views/vetting_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from django.http import Http404, HttpRequest
from ninja import Schema
from ninja.pagination import RouterPaginated

from bats_ai.core.models import VettingDetails

router = RouterPaginated()


class VettingDetailsSchema(Schema):
id: int
user_id: int
reference_materials: str

@classmethod
def from_orm(cls, obj):
print(obj)
return cls(id=obj.id, reference_materials=obj.reference_materials, user_id=obj.user_id)


class UpdateVettingDetailsSchema(Schema):
reference_materials: str


@router.get('/user/{user_id}', response=VettingDetailsSchema)
def get_vetting_details_for_user(request: HttpRequest, user_id: int):
details = VettingDetails.objects.filter(user_id=user_id).first()

if not details:
raise Http404()

if details.user != request.user and not request.user.is_staff:
# Don't leak user IDs, prefer to return a 404 over a 403
raise Http404

return details


@router.post('/user/{user_id}', response=VettingDetailsSchema)
def update_or_create_vetting_details_for_user(
request: HttpRequest,
payload: UpdateVettingDetailsSchema,
user_id: int,
):
if not (request.user.pk == user_id or request.user.is_staff):
raise Http404

details = VettingDetails.objects.filter(user_id=user_id).first()

if not details:
details = VettingDetails(user=request.user, reference_materials=payload.reference_materials)
else:
details.reference_materials = payload.reference_materials

details.save()

return details
14 changes: 12 additions & 2 deletions client/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ export default defineComponent({
const oauthClient = inject<OAuthClient>("oauthClient");
const router = useRouter();
const route = useRoute();
const { nextShared, sharedList, sideTab, loadConfiguration, configuration } = useState();
const {
nextShared,
sharedList,
sideTab,
loadConfiguration,
configuration ,
loadCurrentUser,
loadReviewerMaterials,
} = useState();
const getShared = async () => {
sharedList.value = (await getRecordings(true)).data;
};
Expand All @@ -24,7 +32,9 @@ export default defineComponent({
const checkLogin = async () => {
if (oauthClient.isLoggedIn) {
loginText.value = "Logout";
loadConfiguration();
await loadConfiguration();
await loadCurrentUser();
await loadReviewerMaterials();
if (sharedList.value.length === 0) {
getShared();
}
Expand Down
Loading