diff --git a/bats_ai/core/views/recording_annotation.py b/bats_ai/core/views/recording_annotation.py index 3f6e9fa3..5be4e47f 100644 --- a/bats_ai/core/views/recording_annotation.py +++ b/bats_ai/core/views/recording_annotation.py @@ -206,17 +206,20 @@ def delete_recording_annotation(request: HttpRequest, pk: int): vetting_enabled = ( configuration.mark_annotations_completed_enabled if configuration else False ) - if vetting_enabled and not request.user.is_staff: - raise HttpError( - 403, "Permission denied. Annotations cannot be deleted while vetting is enabled" - ) - annotation = RecordingAnnotation.objects.get(pk=pk) # Check permission: only the annotation owner may delete their own if annotation.owner != request.user: raise HttpError(403, "Permission denied.") + # In vetting mode, non-staff may only delete blank annotations (no species) + if vetting_enabled and not request.user.is_staff and annotation.species.exists(): + raise HttpError( + 403, + "Permission denied. Only blank annotations can be deleted " + "while vetting is enabled.", + ) + annotation.delete() return "Recording annotation deleted successfully." except RecordingAnnotation.DoesNotExist as e: diff --git a/client/src/components/RecordingAnnotationEditor.vue b/client/src/components/RecordingAnnotationEditor.vue index 7f0b359c..63b7a989 100644 --- a/client/src/components/RecordingAnnotationEditor.vue +++ b/client/src/components/RecordingAnnotationEditor.vue @@ -113,6 +113,11 @@ export default defineComponent({ updateAnnotation(); }; + const onSaveComment = (newComment: string) => { + comments.value = newComment; + updateAnnotation(); + }; + const deleteAnnotation = async () => { if (props.annotation && props.recordingId) { deletingAnnotation.value = true; @@ -170,13 +175,11 @@ export default defineComponent({ }); const deleteEnabled = computed(() => { - return ( - props.type !== 'nabat' - && ( - configuration.value.is_admin - || !configuration.value.mark_annotations_completed_enabled - ) - ); + if (props.type === 'nabat') return false; + if (configuration.value.is_admin) return true; + if (!configuration.value.mark_annotations_completed_enabled) return true; + // In vetting mode, non-admins may only delete blank annotations + return speciesEdit.value.length === 0; }); return { @@ -188,6 +191,7 @@ export default defineComponent({ comments, updateAnnotation, onSpeciesModelValue, + onSaveComment, deleteAnnotation, submitAnnotation, confirmSubmitAnnotation, @@ -266,7 +270,11 @@ export default defineComponent({ v-model="speciesEdit" :species-list="species" :disabled="annotation?.submitted || updatingAnnotation || deletingAnnotation" + :vetting-mode="configuration.mark_annotations_completed_enabled" + :annotation-comment="comments" @update:model-value="onSpeciesModelValue" + @delete-blank-annotation="deleteAnnotation" + @save-comment="onSaveComment" /> diff --git a/client/src/components/RecordingAnnotations.vue b/client/src/components/RecordingAnnotations.vue index 6fa5daff..31d7eae1 100644 --- a/client/src/components/RecordingAnnotations.vue +++ b/client/src/components/RecordingAnnotations.vue @@ -145,6 +145,26 @@ export default defineComponent({ return ( currentUserAnnotations.length > 0 && props.type === 'nabat'); }); + const vettingMode = computed(() => configuration.value.mark_annotations_completed_enabled); + // Count all annotations owned by current user (submitted and unsubmitted) for vetting one-annotation limit + const userAnnotationCount = computed(() => + annotations.value.filter( + (a: FileAnnotation) => a.owner === currentUser.value + ).length + ); + const vettingModeAddDisabled = computed(() => + vettingMode.value && userAnnotationCount.value > 0 + ); + const addButtonDisabled = computed(() => + addingAnnotation.value || disableNaBatAnnotations.value || vettingModeAddDisabled.value + ); + const addButtonTooltip = computed(() => { + if (vettingModeAddDisabled.value) { + return 'In vetting mode you may only add one annotation per recording.'; + } + return ''; + }); + function getConfidenceLabelText(confidence: number) { return `Confidence: ${confidence.toFixed(2)}`; } @@ -167,6 +187,11 @@ export default defineComponent({ userSubmittedAnnotationId, handleSubmitAnnotation, configuration, + addButtonDisabled, + addButtonTooltip, + userAnnotationCount, + vettingModeAddDisabled, + vettingMode, }; }, }); @@ -194,8 +219,26 @@ export default defineComponent({ + + + {{ addButtonTooltip }} + diff --git a/client/src/components/SingleSpecieEditor.vue b/client/src/components/SingleSpecieEditor.vue index 321a29ad..529dd35e 100644 --- a/client/src/components/SingleSpecieEditor.vue +++ b/client/src/components/SingleSpecieEditor.vue @@ -99,6 +99,14 @@ export default defineComponent({ onMounted(() => window.addEventListener("keydown", speciesShortcut)); onUnmounted(() => window.removeEventListener("keydown", speciesShortcut)); + const onClearOrDeleteClick = () => { + if (selectedCode.value) { + selectedCode.value = null; + } else { + emit("delete"); + } + }; + return { search, selectedCode, @@ -106,6 +114,7 @@ export default defineComponent({ customFilter, categoryColors, speciesAutocomplete, + onClearOrDeleteClick, }; }, }); @@ -129,7 +138,6 @@ export default defineComponent({ item-value="species_code" :multiple="false" :custom-filter="customFilter" - clearable clear-on-select label="Select species" :menu-props="{ maxHeight: '300px', maxWidth: '400px' }" @@ -138,6 +146,16 @@ export default defineComponent({ density="compact" hide-details > +