From 3b5344970437710a2af1b25e55bc83b61d8f43d8 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Mon, 22 Dec 2025 16:51:37 -0500 Subject: [PATCH 01/11] Add settings for vetting --- ..._annotations_completed_enabled_and_more.py | 28 ++++++++++ bats_ai/core/models/configuration.py | 5 ++ bats_ai/core/views/configuration.py | 6 ++ client/src/api/api.ts | 3 + client/src/views/Admin.vue | 56 ++++++++++++++++++- client/src/views/AdminVetting.vue | 26 +++++++++ 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 bats_ai/core/migrations/0024_configuration_mark_annotations_completed_enabled_and_more.py create mode 100644 client/src/views/AdminVetting.vue diff --git a/bats_ai/core/migrations/0024_configuration_mark_annotations_completed_enabled_and_more.py b/bats_ai/core/migrations/0024_configuration_mark_annotations_completed_enabled_and_more.py new file mode 100644 index 00000000..b4e13f79 --- /dev/null +++ b/bats_ai/core/migrations/0024_configuration_mark_annotations_completed_enabled_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.23 on 2025-12-22 16:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0023_recordingtag_recording_tags_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='configuration', + name='mark_annotations_completed_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='configuration', + name='non_admin_upload_enabled', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='configuration', + name='show_my_recordings', + field=models.BooleanField(default=True), + ), + ] diff --git a/bats_ai/core/models/configuration.py b/bats_ai/core/models/configuration.py index 774831d8..9592f0f2 100644 --- a/bats_ai/core/models/configuration.py +++ b/bats_ai/core/models/configuration.py @@ -32,6 +32,11 @@ class AvailableColorScheme(models.TextChoices): # 18 characters is just enough for "rgb(255, 255, 255)" default_spectrogram_background_color = models.CharField(max_length=18, default='rgb(0, 0, 0)') + # Fields used for community vetting focused deployment of BatAI + non_admin_upload_enabled = models.BooleanField(default=True) + mark_annotations_completed_enabled = models.BooleanField(default=False) + show_my_recordings = models.BooleanField(default=True) + def save(self, *args, **kwargs): # Ensure only one instance of Configuration exists if not Configuration.objects.exists() and not self.pk: diff --git a/bats_ai/core/views/configuration.py b/bats_ai/core/views/configuration.py index 5714688f..12adf2ec 100644 --- a/bats_ai/core/views/configuration.py +++ b/bats_ai/core/views/configuration.py @@ -22,6 +22,9 @@ class ConfigurationSchema(Schema): spectrogram_view: Configuration.SpectrogramViewMode default_color_scheme: Configuration.AvailableColorScheme default_spectrogram_background_color: str + non_admin_upload_enabled: bool + mark_annotations_completed_enabled: bool + show_my_recordings: bool # Endpoint to retrieve the configuration status @@ -38,6 +41,9 @@ def get_configuration(request): spectrogram_view=config.spectrogram_view, default_color_scheme=config.default_color_scheme, default_spectrogram_background_color=config.default_spectrogram_background_color, + non_admin_upload_enabled=config.non_admin_upload_enabled, + mark_annotations_completed_enabled=config.mark_annotations_completed_enabled, + show_my_recordings=config.show_my_recordings, is_admin=request.user.is_authenticated and request.user.is_superuser, ) diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 3b13e655..41f88f0c 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -414,6 +414,9 @@ export interface ConfigurationSettings { is_admin?: boolean; default_color_scheme: string; default_spectrogram_background_color: string; + non_admin_upload_enabled: boolean; + mark_annotations_completed_enabled: boolean; + show_my_recordings: boolean; } export type Configuration = ConfigurationSettings & { is_admin: boolean }; diff --git a/client/src/views/Admin.vue b/client/src/views/Admin.vue index 32a4c5e2..3f8975fc 100644 --- a/client/src/views/Admin.vue +++ b/client/src/views/Admin.vue @@ -15,7 +15,7 @@ export default defineComponent({ }, setup() { // Reactive state for the settings - const tab: Ref<'admin' | 'nabat'> = ref('admin'); + const tab: Ref<'admin' | 'nabat' | 'vetting'> = ref('admin'); const { colorSchemes, configuration, loadConfiguration } = useState(); const settings = reactive({ displayPulseAnnotations: configuration.value.display_pulse_annotations, @@ -25,6 +25,9 @@ export default defineComponent({ spectrogramView: configuration.value.spectrogram_view, defaultColorScheme: configuration.value.default_color_scheme, defaultBackgroundColor: configuration.value.default_spectrogram_background_color, + nonAdminUploadEnabled: configuration.value.non_admin_upload_enabled, + markAnnotationsCompletedEnabled: configuration.value.mark_annotations_completed_enabled, + showMyRecordings: configuration.value.show_my_recordings, }); const spectrogramViewOptions = [ { title: 'Compressed', value: 'compressed' }, @@ -37,7 +40,9 @@ export default defineComponent({ settings.spectrogramXStretch = configuration.value.spectrogram_x_stretch; settings.defaultColorScheme = configuration.value.default_color_scheme; settings.defaultBackgroundColor = configuration.value.default_spectrogram_background_color; - settings.spectrogramView = configuration.value.spectrogram_view; + settings.nonAdminUploadEnabled = configuration.value.non_admin_upload_enabled; + settings.markAnnotationsCompletedEnabled = configuration.value.mark_annotations_completed_enabled; + settings.showMyRecordings = configuration.value.show_my_recordings; }); // Function to save the settings const saveSettings = async () => { @@ -50,6 +55,9 @@ export default defineComponent({ default_color_scheme: settings.defaultColorScheme, default_spectrogram_background_color: settings.defaultBackgroundColor, spectrogram_view: settings.spectrogramView, + non_admin_upload_enabled: settings.nonAdminUploadEnabled, + mark_annotations_completed_enabled: settings.markAnnotationsCompletedEnabled, + show_my_recordings: settings.showMyRecordings, }); loadConfiguration(); }; @@ -83,6 +91,12 @@ export default defineComponent({ > NABat Admin + + Vetting + @@ -180,6 +194,44 @@ export default defineComponent({ + + Vetting + + + + + + + + + + + + + + + Save + + + + diff --git a/client/src/views/AdminVetting.vue b/client/src/views/AdminVetting.vue new file mode 100644 index 00000000..058398c3 --- /dev/null +++ b/client/src/views/AdminVetting.vue @@ -0,0 +1,26 @@ + + + From 49a63203d218a82c6bf4fcfbe0f31000919e690c Mon Sep 17 00:00:00 2001 From: naglepuff Date: Tue, 23 Dec 2025 08:54:05 -0500 Subject: [PATCH 02/11] Model submission status of recording annotations --- bats_ai/core/admin/recording_annotations.py | 1 + .../0025_recordingannotation_submitted.py | 18 +++++++++++++++ bats_ai/core/models/recording_annotation.py | 4 +++- bats_ai/core/views/recording.py | 16 +++++++++++++- bats_ai/core/views/recording_annotation.py | 22 ++++++++++++++++++- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 bats_ai/core/migrations/0025_recordingannotation_submitted.py diff --git a/bats_ai/core/admin/recording_annotations.py b/bats_ai/core/admin/recording_annotations.py index d83c31cb..705033e2 100644 --- a/bats_ai/core/admin/recording_annotations.py +++ b/bats_ai/core/admin/recording_annotations.py @@ -14,6 +14,7 @@ class RecordingAnnotationAdmin(admin.ModelAdmin): 'additional_data', 'comments', 'model', + 'submitted', ] list_select_related = True filter_horizontal = ('species',) # or filter_vertical diff --git a/bats_ai/core/migrations/0025_recordingannotation_submitted.py b/bats_ai/core/migrations/0025_recordingannotation_submitted.py new file mode 100644 index 00000000..ca697ea0 --- /dev/null +++ b/bats_ai/core/migrations/0025_recordingannotation_submitted.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.23 on 2025-12-23 13:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0024_configuration_mark_annotations_completed_enabled_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='recordingannotation', + name='submitted', + field=models.BooleanField(default=False), + ), + ] diff --git a/bats_ai/core/models/recording_annotation.py b/bats_ai/core/models/recording_annotation.py index ab5d9b52..96dafb49 100644 --- a/bats_ai/core/models/recording_annotation.py +++ b/bats_ai/core/models/recording_annotation.py @@ -12,7 +12,8 @@ class RecordingAnnotation(TimeStampedModel, models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE) species = models.ManyToManyField(Species) comments = models.TextField(blank=True, null=True) - model = models.TextField(blank=True, null=True) # AI Model information if inference used + # AI Model information if inference used, else "User Defined" + model = models.TextField(blank=True, null=True) confidence = models.FloatField( default=1.0, validators=[ @@ -24,3 +25,4 @@ class RecordingAnnotation(TimeStampedModel, models.Model): additional_data = models.JSONField( blank=True, null=True, help_text='Additional information about the models/data' ) + submitted = models.BooleanField(default=False) diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 0bd8f35d..5e5076a5 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -76,6 +76,7 @@ class RecordingAnnotationSchema(Schema): confidence: float id: int | None = None hasDetails: bool + submitted: bool @classmethod def from_orm(cls, obj: RecordingAnnotation, **kwargs): @@ -87,6 +88,7 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs): model=obj.model, id=obj.pk, hasDetails=obj.additional_data is not None, + submitted=obj.submitted, ) @@ -246,7 +248,9 @@ def delete_recording( @router.get('/') -def get_recordings(request: HttpRequest, public: bool | None = None): +def get_recordings( + request: HttpRequest, public: bool | None = None, exclude_submitted: bool | None = None +): # Filter recordings based on the owner's id or public=True if public is not None and public: recordings = ( @@ -290,6 +294,16 @@ def get_recordings(request: HttpRequest, public: bool | None = None): ) recording['userMadeAnnotations'] = user_has_annotations + if exclude_submitted: + recordings = [ + recording + for recording in recordings + if not any( + annotation['submitted'] and annotation['owner'] == request.user.username + for annotation in recording['fileAnnotations'] + ) + ] + return list(recordings) diff --git a/bats_ai/core/views/recording_annotation.py b/bats_ai/core/views/recording_annotation.py index e7924c63..564fa22f 100644 --- a/bats_ai/core/views/recording_annotation.py +++ b/bats_ai/core/views/recording_annotation.py @@ -20,6 +20,7 @@ class RecordingAnnotationSchema(Schema): owner: str confidence: float id: int | None = None + submitted: bool hasDetails: bool @classmethod @@ -32,9 +33,10 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs): model=obj.model, id=obj.pk, hasDetails=obj.additional_data is not None, + submitted=obj.submitted ) - +# TODO: do we really need this? why can't we just always return the details? class RecordingAnnotationDetailsSchema(Schema): species: list[SpeciesSchema] | None comments: str | None = None @@ -44,6 +46,7 @@ class RecordingAnnotationDetailsSchema(Schema): id: int | None = None details: dict hasDetails: bool + submitted: bool @classmethod def from_orm(cls, obj: RecordingAnnotation, **kwargs): @@ -56,6 +59,7 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs): hasDetails=obj.additional_data is not None, details=obj.additional_data, id=obj.pk, + submitted=obj.submitted, ) @@ -178,3 +182,19 @@ def delete_recording_annotation(request: HttpRequest, id: int): return 'Recording annotation deleted successfully.' except RecordingAnnotation.DoesNotExist: raise HttpError(404, 'Recording annotation not found.') + + +# Submit endpoint +@router.patch('/{id}/submit', response={200: str}) +def submit_recording_annotation(request: HttpRequest, id: int): + try: + annotation = RecordingAnnotation.objects.get(pk=id) + + # Check permission + if annotation.recording.owner != request.user: + raise HttpError(403, 'Permission denied.') + + annotation.submitted = True + return 'Recording annotation marked as submitted' + except RecordingAnnotation.DoesNotExist: + raise HttpError(404, 'Recording annotation not found.') From 8511dcdd5724efb1cb1ef003c87df088bcba8044 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Tue, 23 Dec 2025 09:49:13 -0500 Subject: [PATCH 03/11] Add endpoint to get current user --- bats_ai/core/views/configuration.py | 13 +++++++++++++ client/src/api/api.ts | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/bats_ai/core/views/configuration.py b/bats_ai/core/views/configuration.py index 12adf2ec..1410cee0 100644 --- a/bats_ai/core/views/configuration.py +++ b/bats_ai/core/views/configuration.py @@ -67,3 +67,16 @@ def check_is_admin(request): if request.user.is_authenticated: return {'is_admin': request.user.is_superuser} return {'is_admin': False} + + +@router.get('/me') +def get_current_user(request): + if request.user.is_authenticated: + return { + 'email': request.user.email, + 'name': request.user.username, + } + return { + 'email': '', + 'name': '' + } diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 41f88f0c..d08b0c32 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -100,6 +100,7 @@ export interface FileAnnotation { confidence: number; hasDetails: boolean; id: number; + submitted: boolean; } export interface FileAnnotationDetails { @@ -428,6 +429,10 @@ async function patchConfiguration(config: ConfigurationSettings) { return axiosInstance.patch("/configuration/", { ...config }); } +async function getCurrentUser() { + return axiosInstance.get<{name: string, email: string}>("/configuration/me"); +} + export interface ProcessingTask { id: number; created: string; @@ -543,4 +548,5 @@ export { getFileAnnotationDetails, getExportStatus, getRecordingTags, + getCurrentUser, }; From 586319822d40096b7fa3765f66071081d888c04a Mon Sep 17 00:00:00 2001 From: naglepuff Date: Tue, 23 Dec 2025 09:49:39 -0500 Subject: [PATCH 04/11] Show recording submission status in table view --- client/src/use/useState.ts | 7 +++ client/src/views/Recordings.vue | 78 +++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/client/src/use/useState.ts b/client/src/use/useState.ts index b6250cbf..7363bde1 100644 --- a/client/src/use/useState.ts +++ b/client/src/use/useState.ts @@ -5,6 +5,7 @@ import * as d3 from "d3"; import { Configuration, getConfiguration, + getCurrentUser, OtherUserAnnotations, Recording, SpectrogramAnnotation, @@ -138,6 +139,11 @@ export default function useState() { configuration.value = (await getConfiguration()).data; } + async function loadCurrentUser() { + const userInfo = (await getCurrentUser()).data; + currentUser.value = userInfo.name; + } + /** * Function used to determine whether or not we are currently looking * at an NABat-specific view. @@ -172,6 +178,7 @@ export default function useState() { currentUser, setSelectedId, loadConfiguration, + loadCurrentUser, isNaBat, // State Passing Elements annotations, diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index 250f087b..5ce8dbdf 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -9,8 +9,10 @@ import { import { deleteRecording, getRecordings, - Recording , + Recording, + FileAnnotation, getRecordingTags, + getConfiguration, } from '../api/api'; import UploadRecording, { EditingRecording } from '@components/UploadRecording.vue'; import MapLocation from '@components/MapLocation.vue'; @@ -30,13 +32,24 @@ export default defineComponent({ }, setup() { const itemsPerPage = ref(-1); - const { sharedList, recordingList, recordingTagList } = useState(); + const { + sharedList, + recordingList, + recordingTagList, + currentUser, + configuration, + loadCurrentUser, + } = useState(); const editingRecording: Ref = ref(null); let intervalRef: number | null = null; const uploadDialog = ref(false); const batchUploadDialog = ref(false); - const headers = ref([ + const headers: Ref<{ + title: string, + key: string, + value?: (item: Recording) => boolean | string | number, + }[]> = ref([ { title:'Name', key: 'name', @@ -200,9 +213,30 @@ export default defineComponent({ return filterTagSet.intersection(itemTagSet).size > 0; }; + function submittedForCurrentUser(recording: Recording) { + const userSubmittedAnnotations = recording.fileAnnotations.filter((annotation: FileAnnotation) => ( + annotation.owner === currentUser.value && annotation.submitted + )); + return userSubmittedAnnotations.length > 0; + } + + function addSubmittedColumn() { + if (configuration.value.mark_annotations_completed_enabled) { + const submittedHeader = { + title: 'Submitted', + key: 'submitted', + value: submittedForCurrentUser, + }; + headers.value.push(submittedHeader); + sharedHeaders.value.push(submittedHeader); + } + } + onMounted(async () => { + await loadCurrentUser(); await fetchRecordingTags(); await fetchRecordings(); + addSubmittedColumn(); }); const uploadDone = () => { @@ -273,6 +307,8 @@ export default defineComponent({ recordingToDelete, editingRecording, dataLoading, + submittedForCurrentUser, + configuration, }; }, }); @@ -469,6 +505,24 @@ export default defineComponent({ mdi-close + + + +