Skip to content

Commit 366944a

Browse files
Allow users to add reference materials (#297)
* Allow users to add reference materials * Model vetting details per user * Update vetting details from front end * Use separate component for reference materials * Add length check and test to API layer * Swap position of close/save for consistency * Use correct max length value for vuetify rule Co-authored-by: Bryon Lewis <61746913+BryonLewis@users.noreply.github.com> --------- Co-authored-by: Bryon Lewis <61746913+BryonLewis@users.noreply.github.com>
1 parent 9ed4aaa commit 366944a

18 files changed

Lines changed: 435 additions & 7 deletions

bats_ai/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
RecordingRouter,
1414
RecordingTagRouter,
1515
SpeciesRouter,
16+
VettingRouter,
1617
)
1718
from bats_ai.core.views.nabat import NABatConfigurationRouter, NABatRecordingRouter
1819

@@ -46,3 +47,4 @@ def global_auth(request):
4647
api.add_router('/recording-tag/', RecordingTagRouter)
4748
api.add_router('/nabat/recording/', NABatRecordingRouter)
4849
api.add_router('/nabat/configuration/', NABatConfigurationRouter)
50+
api.add_router('/vetting/', VettingRouter)

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 .vetting_details import VettingDetailsAdmin
2122

2223
__all__ = [
2324
'AnnotationsAdmin',
@@ -34,6 +35,7 @@
3435
'ConfigurationAdmin',
3536
'ExportedAnnotationFileAdmin',
3637
'SpectrogramImageAdmin',
38+
'VettingDetailsAdmin',
3739
# NABat Models
3840
'NABatRecordingAnnotationAdmin',
3941
'NABatCompressedSpectrogramAdmin',
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.contrib import admin
2+
3+
from bats_ai.core.models import VettingDetails
4+
5+
6+
@admin.register(VettingDetails)
7+
class VettingDetailsAdmin(admin.ModelAdmin):
8+
list_display = [
9+
'pk',
10+
'user',
11+
# 'reference_materials',
12+
]
13+
search_fields = ('user',)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Generated by Django 4.2.23 on 2026-01-08 18:57
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('core', '0024_configuration_mark_annotations_completed_enabled_and_more'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='VettingDetails',
18+
fields=[
19+
(
20+
'id',
21+
models.BigAutoField(
22+
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
23+
),
24+
),
25+
('reference_materials', models.TextField(blank=True)),
26+
(
27+
'user',
28+
models.OneToOneField(
29+
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
30+
),
31+
),
32+
],
33+
),
34+
migrations.AddConstraint(
35+
model_name='vettingdetails',
36+
constraint=models.CheckConstraint(
37+
check=models.Q(('reference_materials__length__lte', 2000)),
38+
name='reference_materials_max_2000',
39+
),
40+
),
41+
]

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 .vetting_details import VettingDetails
1516

1617
__all__ = [
1718
'Annotations',
@@ -30,4 +31,5 @@
3031
'ProcessingTaskType',
3132
'ExportedAnnotationFile',
3233
'SpectrogramImage',
34+
'VettingDetails',
3335
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.contrib.auth.models import User
2+
from django.db import models
3+
from django.db.models import Q
4+
from django.db.models.functions import Length
5+
6+
models.TextField.register_lookup(Length, 'length')
7+
8+
9+
class VettingDetails(models.Model):
10+
user = models.OneToOneField(User, on_delete=models.CASCADE)
11+
reference_materials = models.TextField(blank=True)
12+
13+
class Meta:
14+
constraints = [
15+
models.CheckConstraint(
16+
# TODO change to 'condition' in Django v6
17+
check=Q(reference_materials__length__lte=2000),
18+
name='reference_materials_max_2000',
19+
)
20+
]

bats_ai/core/tests/conftest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from django.test import Client
33
import pytest
44

5-
from .factories import SuperuserFactory, UserFactory
5+
from bats_ai.core.models import VettingDetails
6+
7+
from .factories import SuperuserFactory, UserFactory, VettingDetailsFactory
68

79

810
@pytest.fixture
@@ -32,3 +34,13 @@ def authorized_client(superuser: User) -> Client:
3234
client = Client()
3335
client.force_login(user=superuser)
3436
return client
37+
38+
39+
@pytest.fixture
40+
def vetting_details(user: User) -> VettingDetails:
41+
return VettingDetailsFactory(user=user)
42+
43+
44+
@pytest.fixture
45+
def random_user_vetting_details() -> VettingDetails:
46+
return VettingDetailsFactory(user=UserFactory())

bats_ai/core/tests/factories.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.contrib.auth.models import User
22
import factory.django
33

4+
from bats_ai.core.models import VettingDetails
5+
46

57
class UserFactory(factory.django.DjangoModelFactory[User]):
68
class Meta:
@@ -28,3 +30,12 @@ class Meta:
2830
def _create(cls, model_class, *args, **kwargs):
2931
manager = cls._get_manager(model_class)
3032
return manager.create_superuser(*args, **kwargs)
33+
34+
35+
class VettingDetailsFactory(factory.django.DjangoModelFactory[VettingDetails]):
36+
37+
class Meta:
38+
model = VettingDetails
39+
40+
user = factory.SubFactory(UserFactory)
41+
reference_materials = factory.Faker('paragraph', nb_sentences=3)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import pytest
2+
3+
from .factories import UserFactory, VettingDetailsFactory
4+
5+
6+
@pytest.mark.parametrize(
7+
'client_fixture,status_code',
8+
[
9+
('client', 401),
10+
('authenticated_client', 200),
11+
('authorized_client', 200),
12+
],
13+
)
14+
@pytest.mark.django_db
15+
def test_get_vetting_details(client_fixture, status_code, user, vetting_details, request):
16+
api_client = request.getfixturevalue(client_fixture)
17+
resp = api_client.get(f'/api/v1/vetting/user/{user.id}')
18+
assert resp.status_code == status_code
19+
if status_code == 200:
20+
assert resp.json()['reference_materials'] == vetting_details.reference_materials
21+
22+
23+
@pytest.mark.django_db
24+
def test_get_vetting_details_other_user(authenticated_client):
25+
other_user = UserFactory()
26+
VettingDetailsFactory(user=other_user)
27+
resp = authenticated_client.get(f'/api/v1/vetting/user/{other_user.id}')
28+
assert resp.status_code == 404
29+
30+
31+
@pytest.mark.django_db
32+
def test_create_vetting_details(client):
33+
test_text = 'foo'
34+
data = {'reference_materials': test_text}
35+
test_user = UserFactory()
36+
client.force_login(user=test_user)
37+
resp = client.post(
38+
f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json'
39+
)
40+
assert resp.status_code == 200
41+
assert resp.json()['user_id'] == test_user.id
42+
43+
44+
@pytest.mark.parametrize(
45+
'client_fixture,status_code',
46+
[
47+
('authenticated_client', 404),
48+
('authorized_client', 200),
49+
],
50+
)
51+
@pytest.mark.django_db
52+
def test_create_vetting_details_other_user(client_fixture, status_code, request):
53+
api_client = request.getfixturevalue(client_fixture)
54+
test_text = 'foo'
55+
data = {'reference_materials': test_text}
56+
other_user = UserFactory()
57+
resp = api_client.post(
58+
f'/api/v1/vetting/user/{other_user.id}', data=data, content_type='application/json'
59+
)
60+
assert resp.status_code == status_code
61+
if status_code == 200:
62+
assert resp.json()['reference_materials'] == test_text
63+
64+
65+
@pytest.mark.django_db
66+
def test_update_vetting_details(client):
67+
test_text = 'bar'
68+
data = {'reference_materials': 'bar'}
69+
test_user = UserFactory()
70+
VettingDetailsFactory(user=test_user, reference_materials='foo')
71+
client.force_login(test_user)
72+
73+
initial_resp = client.get(f'/api/v1/vetting/user/{test_user.id}')
74+
assert initial_resp.status_code == 200
75+
76+
resp = client.post(
77+
f'/api/v1/vetting/user/{test_user.id}', data=data, content_type='application/json'
78+
)
79+
assert resp.status_code == 200
80+
81+
new_details_response = client.get(f'/api/v1/vetting/user/{test_user.id}')
82+
assert new_details_response.status_code == 200
83+
assert new_details_response.json()['reference_materials'] == test_text
84+
85+
86+
@pytest.mark.parametrize(
87+
'client_fixture,status_code',
88+
[
89+
('authenticated_client', 404),
90+
('authorized_client', 200),
91+
],
92+
)
93+
@pytest.mark.django_db
94+
def test_update_vetting_details_other_user(
95+
client_fixture, status_code, random_user_vetting_details, request
96+
):
97+
api_client = request.getfixturevalue(client_fixture)
98+
resp = api_client.post(
99+
f'/api/v1/vetting/user/{random_user_vetting_details.user.id}',
100+
data={'reference_materials': 'foo'},
101+
content_type='application/json',
102+
)
103+
assert resp.status_code == status_code
104+
105+
106+
@pytest.mark.django_db
107+
def test_update_vetting_details_length_constraint(authorized_client, random_user_vetting_details):
108+
data = {'reference_materials': 'a' * 2001}
109+
resp = authorized_client.post(
110+
f'/api/v1/vetting/user/{random_user_vetting_details.user.id}',
111+
data=data,
112+
content_type='application/json',
113+
)
114+
assert resp.status_code == 400

bats_ai/core/views/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .recording_tag import router as RecordingTagRouter
1010
from .sequence_annotations import router as SequenceAnnotationRouter
1111
from .species import router as SpeciesRouter
12+
from .vetting_details import router as VettingRouter
1213

1314
__all__ = [
1415
'RecordingRouter',
@@ -22,4 +23,5 @@
2223
'ProcessingTaskRouter',
2324
'ExportAnnotationRouter',
2425
'RecordingTagRouter',
26+
'VettingRouter',
2527
]

0 commit comments

Comments
 (0)