From 297c1985df555b6fad9dbfc10fa776d4484501ef Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 8 Jan 2026 10:57:26 -0500 Subject: [PATCH 1/5] Allow users to add reference materials --- client/src/use/useState.ts | 3 ++ client/src/views/Spectrogram.vue | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/client/src/use/useState.ts b/client/src/use/useState.ts index cd5fba6..ac1a69a 100644 --- a/client/src/use/useState.ts +++ b/client/src/use/useState.ts @@ -288,6 +288,8 @@ export default function useState() { return undefined; }); + const reviewerMaterials = ref(''); + return { annotationState, creationType, @@ -341,5 +343,6 @@ export default function useState() { previousUnsubmittedRecordingId, markAnnotationSubmitted, currentRecordingId, + reviewerMaterials, }; } diff --git a/client/src/views/Spectrogram.vue b/client/src/views/Spectrogram.vue index 5c4830a..4d0243e 100644 --- a/client/src/views/Spectrogram.vue +++ b/client/src/views/Spectrogram.vue @@ -9,6 +9,7 @@ import { watch, } from "vue"; import { useRouter } from "vue-router"; +import { debounce } from "lodash"; import { getSpecies, getAnnotations, @@ -74,6 +75,7 @@ export default defineComponent({ nextUnsubmittedRecordingId, previousUnsubmittedRecordingId, currentRecordingId, + reviewerMaterials, } = useState(); const router = useRouter(); const images: Ref = ref([]); @@ -278,6 +280,14 @@ export default defineComponent({ router.push({ path: `/recording/${previousUnsubmittedRecordingId.value}/spectrogram`, replace: true }); } + const referenceDialog = ref(false); + + function _saveReviewerMaterials() { + console.log(reviewerMaterials.value); + } + + const saveReviewerMaterials = debounce(_saveReviewerMaterials, 500); + return { configuration, annotationState, @@ -327,6 +337,9 @@ export default defineComponent({ goToNextUnreviewed, goToPreviousUnreviewed, nextUnsubmittedRecordingId, + referenceDialog, + reviewerMaterials, + saveReviewerMaterials, }; }, }); @@ -656,6 +669,41 @@ export default defineComponent({ + + + + + + + Reference Materials + + + + + + + Close + + + + + + Date: Thu, 8 Jan 2026 17:52:58 -0500 Subject: [PATCH 2/5] Model vetting details per user --- bats_ai/api.py | 2 + bats_ai/core/admin/__init__.py | 2 + bats_ai/core/admin/vetting_details.py | 13 +++ .../0025_vettingdetails_and_more.py | 41 +++++++ bats_ai/core/models/__init__.py | 2 + bats_ai/core/models/vetting_details.py | 20 ++++ bats_ai/core/tests/conftest.py | 14 ++- bats_ai/core/tests/factories.py | 11 ++ bats_ai/core/tests/test_vetting_details.py | 103 ++++++++++++++++++ bats_ai/core/views/__init__.py | 2 + bats_ai/core/views/vetting_details.py | 57 ++++++++++ 11 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 bats_ai/core/admin/vetting_details.py create mode 100644 bats_ai/core/migrations/0025_vettingdetails_and_more.py create mode 100644 bats_ai/core/models/vetting_details.py create mode 100644 bats_ai/core/tests/test_vetting_details.py create mode 100644 bats_ai/core/views/vetting_details.py diff --git a/bats_ai/api.py b/bats_ai/api.py index 4cd1c0b..9b1cc1c 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -13,6 +13,7 @@ RecordingRouter, RecordingTagRouter, SpeciesRouter, + VettingRouter, ) from bats_ai.core.views.nabat import NABatConfigurationRouter, NABatRecordingRouter @@ -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) diff --git a/bats_ai/core/admin/__init__.py b/bats_ai/core/admin/__init__.py index fe5a419..748798f 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 .vetting_details import VettingDetailsAdmin __all__ = [ 'AnnotationsAdmin', @@ -34,6 +35,7 @@ 'ConfigurationAdmin', 'ExportedAnnotationFileAdmin', 'SpectrogramImageAdmin', + 'VettingDetailsAdmin', # NABat Models 'NABatRecordingAnnotationAdmin', 'NABatCompressedSpectrogramAdmin', diff --git a/bats_ai/core/admin/vetting_details.py b/bats_ai/core/admin/vetting_details.py new file mode 100644 index 0000000..a350c87 --- /dev/null +++ b/bats_ai/core/admin/vetting_details.py @@ -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',) diff --git a/bats_ai/core/migrations/0025_vettingdetails_and_more.py b/bats_ai/core/migrations/0025_vettingdetails_and_more.py new file mode 100644 index 0000000..967f548 --- /dev/null +++ b/bats_ai/core/migrations/0025_vettingdetails_and_more.py @@ -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', + ), + ), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index ad11fab..01dee04 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 .vetting_details import VettingDetails __all__ = [ 'Annotations', @@ -30,4 +31,5 @@ 'ProcessingTaskType', 'ExportedAnnotationFile', 'SpectrogramImage', + 'VettingDetails', ] diff --git a/bats_ai/core/models/vetting_details.py b/bats_ai/core/models/vetting_details.py new file mode 100644 index 0000000..41f84fc --- /dev/null +++ b/bats_ai/core/models/vetting_details.py @@ -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', + ) + ] diff --git a/bats_ai/core/tests/conftest.py b/bats_ai/core/tests/conftest.py index d6545dc..d1c5e8f 100644 --- a/bats_ai/core/tests/conftest.py +++ b/bats_ai/core/tests/conftest.py @@ -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 @@ -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()) diff --git a/bats_ai/core/tests/factories.py b/bats_ai/core/tests/factories.py index ccb88e7..708fbac 100644 --- a/bats_ai/core/tests/factories.py +++ b/bats_ai/core/tests/factories.py @@ -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: @@ -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) diff --git a/bats_ai/core/tests/test_vetting_details.py b/bats_ai/core/tests/test_vetting_details.py new file mode 100644 index 0000000..053e603 --- /dev/null +++ b/bats_ai/core/tests/test_vetting_details.py @@ -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 diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index 0be8ef8..ea390e4 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -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', @@ -22,4 +23,5 @@ 'ProcessingTaskRouter', 'ExportAnnotationRouter', 'RecordingTagRouter', + 'VettingRouter', ] diff --git a/bats_ai/core/views/vetting_details.py b/bats_ai/core/views/vetting_details.py new file mode 100644 index 0000000..484e554 --- /dev/null +++ b/bats_ai/core/views/vetting_details.py @@ -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 From a1400c09d5654dd419db449112dfe3d565ce94ef Mon Sep 17 00:00:00 2001 From: naglepuff Date: Fri, 9 Jan 2026 14:08:03 -0500 Subject: [PATCH 3/5] Update vetting details from front end --- bats_ai/core/views/configuration.py | 1 + client/src/App.vue | 14 ++++++++++-- client/src/api/api.ts | 35 ++++++++++++++++++++++++++++- client/src/use/useState.ts | 18 ++++++++++++++- client/src/views/Recordings.vue | 2 -- client/src/views/Spectrogram.vue | 5 ++++- 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/bats_ai/core/views/configuration.py b/bats_ai/core/views/configuration.py index 494beb5..be7c66f 100644 --- a/bats_ai/core/views/configuration.py +++ b/bats_ai/core/views/configuration.py @@ -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': ''} diff --git a/client/src/App.vue b/client/src/App.vue index 3f6ae30..c7d25f3 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -12,7 +12,15 @@ export default defineComponent({ const oauthClient = inject("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; }; @@ -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(); } diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 4faae6f..497fff5 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { AxiosError } from "axios"; import { SpectroInfo } from "@components/geoJS/geoJSUtils"; export interface Recording { @@ -453,7 +454,7 @@ async function patchConfiguration(config: ConfigurationSettings) { } async function getCurrentUser() { - return axiosInstance.get<{name: string, email: string}>("/configuration/me"); + return axiosInstance.get<{name: string, email: string, id: number}>("/configuration/me"); } export interface ProcessingTask { @@ -537,6 +538,36 @@ async function getExportStatus(exportId: number) { return result.data; } +export interface VettingDetails { + id: number; + user_id: number; + reference_materials: string; +} + +export interface UpdateVettingDetails { + reference_Materials: string; +} + +async function getVettingDetailsForUser(userId: number) { + try { + const result = await axiosInstance.get(`/vetting/user/${userId}`); + return result.data; + } catch (err) { + const error = err as AxiosError; + if (error.response?.status === 404) { + return null; + } + throw err; + } +} + +async function createOrUpdateVettingDetailsForUser(userId: number, referenceMaterials: string) { + return await axiosInstance.post( + `/vetting/user/${userId}`, + { 'reference_materials': referenceMaterials } + ); +} + export { uploadRecordingFile, getRecordings, @@ -574,4 +605,6 @@ export { getExportStatus, getRecordingTags, getCurrentUser, + getVettingDetailsForUser, + createOrUpdateVettingDetailsForUser, }; diff --git a/client/src/use/useState.ts b/client/src/use/useState.ts index ac1a69a..d17f7ac 100644 --- a/client/src/use/useState.ts +++ b/client/src/use/useState.ts @@ -12,6 +12,7 @@ import { SpectrogramSequenceAnnotation, RecordingTag, FileAnnotation, + getVettingDetailsForUser, } from "../api/api"; import { interpolateCividis, @@ -41,6 +42,7 @@ const colorScheme: Ref<{ value: string; title: string; scheme: (input: number) = const backgroundColor = ref("rgb(0, 0, 0)"); const selectedUsers: Ref = ref([]); const currentUser: Ref = ref(""); +const currentUserId: Ref = ref(undefined); const selectedId: Ref = ref(null); const selectedType: Ref<"pulse" | "sequence"> = ref("pulse"); const annotations: Ref = ref([]); @@ -85,6 +87,8 @@ const toggleFixedAxes = () => { fixedAxes.value = !fixedAxes.value; }; +const reviewerMaterials = ref(''); + type AnnotationState = "" | "editing" | "creating" | "disabled"; export default function useState() { const setAnnotationState = (state: AnnotationState) => { @@ -147,6 +151,7 @@ export default function useState() { async function loadCurrentUser() { const userInfo = (await getCurrentUser()).data; currentUser.value = userInfo.name; + currentUserId.value = userInfo.id; } /** @@ -288,7 +293,16 @@ export default function useState() { return undefined; }); - const reviewerMaterials = ref(''); + async function loadReviewerMaterials() { + // Only make this request if vetting is enabled and a user is logged in + if (!configuration.value.mark_annotations_completed_enabled) return; + if (currentUserId.value === undefined) return; + + const vettingDetails = await getVettingDetailsForUser(currentUserId.value); + if (vettingDetails) { + reviewerMaterials.value = vettingDetails.reference_materials; + } + } return { annotationState, @@ -310,6 +324,7 @@ export default function useState() { setSelectedUsers, selectedUsers, currentUser, + currentUserId, setSelectedId, loadConfiguration, loadCurrentUser, @@ -344,5 +359,6 @@ export default function useState() { markAnnotationSubmitted, currentRecordingId, reviewerMaterials, + loadReviewerMaterials, }; } diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index ef15dbc..dc3c29d 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -37,7 +37,6 @@ export default defineComponent({ recordingTagList, currentUser, configuration, - loadCurrentUser, showSubmittedRecordings, submittedMyRecordings, submittedSharedRecordings, @@ -271,7 +270,6 @@ export default defineComponent({ onMounted(async () => { addSubmittedColumns(); hideDetailedMetadataColumns(); - await loadCurrentUser(); await fetchRecordingTags(); await fetchRecordings(); }); diff --git a/client/src/views/Spectrogram.vue b/client/src/views/Spectrogram.vue index 4d0243e..7812430 100644 --- a/client/src/views/Spectrogram.vue +++ b/client/src/views/Spectrogram.vue @@ -18,6 +18,7 @@ import { getSpectrogramCompressed, getOtherUserAnnotations, getSequenceAnnotations, + createOrUpdateVettingDetailsForUser, } from "../api/api"; import SpectrogramViewer from "@components/SpectrogramViewer.vue"; import { SpectroInfo } from "@components/geoJS/geoJSUtils"; @@ -75,6 +76,7 @@ export default defineComponent({ nextUnsubmittedRecordingId, previousUnsubmittedRecordingId, currentRecordingId, + currentUserId, reviewerMaterials, } = useState(); const router = useRouter(); @@ -283,7 +285,8 @@ export default defineComponent({ const referenceDialog = ref(false); function _saveReviewerMaterials() { - console.log(reviewerMaterials.value); + if (!currentUserId.value) return; + createOrUpdateVettingDetailsForUser(currentUserId.value, reviewerMaterials.value); } const saveReviewerMaterials = debounce(_saveReviewerMaterials, 500); From 2dff11ebf2eebe14eb61cd0cb7ec0c77e5cada29 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Mon, 12 Jan 2026 17:58:15 -0500 Subject: [PATCH 4/5] Use separate component for reference materials --- .../components/ReferenceMaterialsDialog.vue | 79 +++++++++++++++++++ client/src/views/Spectrogram.vue | 52 +----------- 2 files changed, 83 insertions(+), 48 deletions(-) create mode 100644 client/src/components/ReferenceMaterialsDialog.vue diff --git a/client/src/components/ReferenceMaterialsDialog.vue b/client/src/components/ReferenceMaterialsDialog.vue new file mode 100644 index 0000000..4b1e7d1 --- /dev/null +++ b/client/src/components/ReferenceMaterialsDialog.vue @@ -0,0 +1,79 @@ + + + diff --git a/client/src/views/Spectrogram.vue b/client/src/views/Spectrogram.vue index 7812430..cc4f8d2 100644 --- a/client/src/views/Spectrogram.vue +++ b/client/src/views/Spectrogram.vue @@ -9,7 +9,6 @@ import { watch, } from "vue"; import { useRouter } from "vue-router"; -import { debounce } from "lodash"; import { getSpecies, getAnnotations, @@ -18,7 +17,6 @@ import { getSpectrogramCompressed, getOtherUserAnnotations, getSequenceAnnotations, - createOrUpdateVettingDetailsForUser, } from "../api/api"; import SpectrogramViewer from "@components/SpectrogramViewer.vue"; import { SpectroInfo } from "@components/geoJS/geoJSUtils"; @@ -27,8 +25,9 @@ import ThumbnailViewer from "@components/ThumbnailViewer.vue"; import RecordingList from "@components/RecordingList.vue"; import OtherUserAnnotationsDialog from "@/components/OtherUserAnnotationsDialog.vue"; import ColorSchemeDialog from "@/components/ColorSchemeDialog.vue"; -import useState from "@use/useState"; import RecordingInfoDialog from "@components/RecordingInfoDialog.vue"; +import ReferenceMaterialsDialog from "@/components/ReferenceMaterialsDialog.vue"; +import useState from "@use/useState"; export default defineComponent({ name: "Spectrogram", components: { @@ -39,6 +38,7 @@ export default defineComponent({ RecordingList, OtherUserAnnotationsDialog, ColorSchemeDialog, + ReferenceMaterialsDialog, }, props: { id: { @@ -76,8 +76,6 @@ export default defineComponent({ nextUnsubmittedRecordingId, previousUnsubmittedRecordingId, currentRecordingId, - currentUserId, - reviewerMaterials, } = useState(); const router = useRouter(); const images: Ref = ref([]); @@ -282,15 +280,6 @@ export default defineComponent({ router.push({ path: `/recording/${previousUnsubmittedRecordingId.value}/spectrogram`, replace: true }); } - const referenceDialog = ref(false); - - function _saveReviewerMaterials() { - if (!currentUserId.value) return; - createOrUpdateVettingDetailsForUser(currentUserId.value, reviewerMaterials.value); - } - - const saveReviewerMaterials = debounce(_saveReviewerMaterials, 500); - return { configuration, annotationState, @@ -340,9 +329,6 @@ export default defineComponent({ goToNextUnreviewed, goToPreviousUnreviewed, nextUnsubmittedRecordingId, - referenceDialog, - reviewerMaterials, - saveReviewerMaterials, }; }, }); @@ -674,37 +660,7 @@ export default defineComponent({ - - - - - Reference Materials - - - - - - - Close - - - - + From 8db34c73fe0b967e531dba711313f9e3defffa35 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Mon, 12 Jan 2026 18:12:05 -0500 Subject: [PATCH 5/5] Add length check and test to API layer --- bats_ai/core/tests/test_vetting_details.py | 12 ++++++++++-- bats_ai/core/views/vetting_details.py | 7 ++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/bats_ai/core/tests/test_vetting_details.py b/bats_ai/core/tests/test_vetting_details.py index 053e603..c94d930 100644 --- a/bats_ai/core/tests/test_vetting_details.py +++ b/bats_ai/core/tests/test_vetting_details.py @@ -96,8 +96,16 @@ def test_update_vetting_details_other_user( ): api_client = request.getfixturevalue(client_fixture) resp = api_client.post( + ) + assert resp.status_code == status_code + + +@pytest.mark.django_db +def test_update_vetting_details_length_constraint(authorized_client, random_user_vetting_details): + data = {'reference_materials': 'a' * 2001} + resp = authorized_client.post( f'/api/v1/vetting/user/{random_user_vetting_details.user.id}', - data={'reference_materials': 'foo'}, + data=data, content_type='application/json', ) - assert resp.status_code == status_code + assert resp.status_code == 400 diff --git a/bats_ai/core/views/vetting_details.py b/bats_ai/core/views/vetting_details.py index 484e554..38a9d5e 100644 --- a/bats_ai/core/views/vetting_details.py +++ b/bats_ai/core/views/vetting_details.py @@ -1,4 +1,4 @@ -from django.http import Http404, HttpRequest +from django.http import Http404, HttpRequest, HttpResponseBadRequest from ninja import Schema from ninja.pagination import RouterPaginated @@ -45,6 +45,11 @@ def update_or_create_vetting_details_for_user( if not (request.user.pk == user_id or request.user.is_staff): raise Http404 + if len(payload.reference_materials) > 2000: + return HttpResponseBadRequest( + 'reference_materials exceeds maximum length of 2000 characters' + ) + details = VettingDetails.objects.filter(user_id=user_id).first() if not details: