diff --git a/bats_ai/core/admin/recording_annotations.py b/bats_ai/core/admin/recording_annotations.py index d83c31c..705033e 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/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 0000000..110c58c --- /dev/null +++ b/bats_ai/core/migrations/0024_configuration_mark_annotations_completed_enabled_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.23 on 2025-12-23 20:18 + +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), + ), + migrations.AddField( + model_name='recordingannotation', + name='submitted', + field=models.BooleanField(default=False), + ), + ] diff --git a/bats_ai/core/models/configuration.py b/bats_ai/core/models/configuration.py index 774831d..9592f0f 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/models/recording_annotation.py b/bats_ai/core/models/recording_annotation.py index ab5d9b5..96dafb4 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/configuration.py b/bats_ai/core/views/configuration.py index 5714688..494beb5 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, ) @@ -61,3 +67,13 @@ 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/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 0bd8f35..5e5076a 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 e7924c6..ad56e84 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,11 @@ 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 +47,7 @@ class RecordingAnnotationDetailsSchema(Schema): id: int | None = None details: dict hasDetails: bool + submitted: bool @classmethod def from_orm(cls, obj: RecordingAnnotation, **kwargs): @@ -56,6 +60,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 +183,23 @@ 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: dict}) +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 + annotation.save() + return { + 'id': id, + 'submitted': annotation.submitted, + } + except RecordingAnnotation.DoesNotExist: + raise HttpError(404, 'Recording annotation not found.') diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 3b13e65..246ce16 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 { @@ -395,6 +396,12 @@ async function deleteFileAnnotation(fileAnnotationId: number) { ); } +async function submitFileAnnotation(fileAnnotationId: number) { + return axiosInstance.patch<{ id: number, submitted: boolean }>( + `recording-annotation/${fileAnnotationId}/submit` + ); +} + interface CellIDReponse { grid_cell_id?: number; error?: string; @@ -414,6 +421,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 }; @@ -425,6 +435,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; @@ -531,6 +545,7 @@ export { putFileAnnotation, patchFileAnnotation, deleteFileAnnotation, + submitFileAnnotation, getConfiguration, patchConfiguration, getProcessingTasks, @@ -540,4 +555,5 @@ export { getFileAnnotationDetails, getExportStatus, getRecordingTags, + getCurrentUser, }; diff --git a/client/src/components/RecordingAnnotationEditor.vue b/client/src/components/RecordingAnnotationEditor.vue index d731e51..c5fe68a 100644 --- a/client/src/components/RecordingAnnotationEditor.vue +++ b/client/src/components/RecordingAnnotationEditor.vue @@ -1,8 +1,16 @@