Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions bats_ai/core/views/recording_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 15 additions & 7 deletions client/src/components/RecordingAnnotationEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -188,6 +191,7 @@ export default defineComponent({
comments,
updateAnnotation,
onSpeciesModelValue,
onSaveComment,
deleteAnnotation,
submitAnnotation,
confirmSubmitAnnotation,
Expand Down Expand Up @@ -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"
/>
</v-row>
<v-row v-if="type === 'nabat'">
Expand Down
45 changes: 44 additions & 1 deletion client/src/components/RecordingAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
}
Expand All @@ -167,6 +187,11 @@ export default defineComponent({
userSubmittedAnnotationId,
handleSubmitAnnotation,
configuration,
addButtonDisabled,
addButtonTooltip,
userAnnotationCount,
vettingModeAddDisabled,
vettingMode,
};
},
});
Expand Down Expand Up @@ -194,8 +219,26 @@ export default defineComponent({
</v-col>
<v-spacer />
<v-col v-if="!isNaBat() || !disableNaBatAnnotations">
<v-tooltip
v-if="addButtonTooltip"
location="bottom"
>
<template #activator="{ props: tooltipProps }">
<div v-bind="tooltipProps">
<v-btn
:disabled="addButtonDisabled"
:loading="addingAnnotation"
@click="addAnnotation()"
>
Add<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</template>
{{ addButtonTooltip }}
</v-tooltip>
<v-btn
:disabled="addingAnnotation || disableNaBatAnnotations"
v-else
:disabled="addButtonDisabled"
:loading="addingAnnotation"
@click="addAnnotation()"
>
Expand Down
20 changes: 19 additions & 1 deletion client/src/components/SingleSpecieEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,22 @@ 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,
groupedItems,
customFilter,
categoryColors,
speciesAutocomplete,
onClearOrDeleteClick,
};
},
});
Expand All @@ -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' }"
Expand All @@ -138,6 +146,16 @@ export default defineComponent({
density="compact"
hide-details
>
<template #append-inner>
<v-icon
size="small"
class="cursor-pointer"
:title="selectedCode ? 'Clear species' : 'Delete blank annotation'"
@click.stop="onClearOrDeleteClick"
>
mdi-close
</v-icon>
</template>
<template #subheader="{ props: subProps }">
<v-list-subheader
class="font-weight-bold"
Expand Down
144 changes: 128 additions & 16 deletions client/src/components/SpeciesEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,25 @@ export default defineComponent({
type: Boolean,
default: false,
},
vettingMode: {
type: Boolean,
default: false,
},
annotationComment: {
type: String,
default: "",
},

},
emits: ["update:modelValue"],
emits: ["update:modelValue", "deleteBlankAnnotation", "saveComment"],
setup(props, { emit }) {
// Internal: one slot per row, at least one. Empty string = no selection in that slot.
const localSpeciesList = ref<string[]>(
props.modelValue?.length ? [...props.modelValue] : [""]
);
const addSpeciesConfirmOpen = ref(false);
const commentDialogOpen = ref(false);
const commentDraft = ref("");

watch(
() => props.modelValue,
Expand All @@ -54,6 +65,14 @@ export default defineComponent({
}
}

function onSlotDelete(index: number) {
if (localSpeciesList.value.length <= 1 && (localSpeciesList.value[0] ?? "") === "") {
emit("deleteBlankAnnotation");
} else {
removeSpecies(index);
}
}

function openAddSpeciesConfirm() {
addSpeciesConfirmOpen.value = true;
}
Expand All @@ -75,14 +94,41 @@ export default defineComponent({
emitValue();
}

function openCommentDialog() {
commentDraft.value = props.annotationComment ?? "";
commentDialogOpen.value = true;
}

function closeCommentDialog() {
commentDialogOpen.value = false;
}

function saveComment() {
emit("saveComment", commentDraft.value);
closeCommentDialog();
}

function removeComment() {
commentDraft.value = "";
emit("saveComment", "");
closeCommentDialog();
}

return {
localSpeciesList,
onSlotUpdate,
onSlotDelete,
openAddSpeciesConfirm,
closeAddSpeciesConfirm,
confirmAddSpecies,
addSpeciesConfirmOpen,
removeSpecies,
commentDialogOpen,
commentDraft,
openCommentDialog,
closeCommentDialog,
saveComment,
removeComment,
};
},
});
Expand All @@ -101,25 +147,46 @@ export default defineComponent({
:disabled="disabled"
:show-delete="localSpeciesList.length > 1"
@update:model-value="onSlotUpdate(index, $event)"
@delete="removeSpecies(index)"
@delete="onSlotDelete(index)"
/>
</div>
<v-btn
v-tooltip="'Add another bat'"
size="small"
variant="outlined"
color="primary"
:disabled="disabled"
<v-row
dense
class="mt-1 mb-2"
@click="openAddSpeciesConfirm"
>
<v-icon start>
<v-icon>
mdi-plus
</v-icon>
</v-icon>
Add Bat
</v-btn>
<v-col>
<v-btn
v-tooltip="'Add another bat'"
size="small"
variant="outlined"
color="primary"
:disabled="disabled"
@click="openAddSpeciesConfirm"
>
<v-icon start>
mdi-plus
</v-icon>
Add Bat
</v-btn>
</v-col>
<v-spacer />
<v-col>
<v-btn
v-if="vettingMode"
v-tooltip="annotationComment ? `Edit comment: ${annotationComment}` : 'Add optional comment to this annotation'"
size="small"
variant="outlined"
color="primary"
:disabled="disabled"
@click="openCommentDialog"
>
<v-icon start>
{{ annotationComment ? 'mdi-pencil' : 'mdi-plus' }}
</v-icon>
Comment
</v-btn>
</v-col>
</v-row>
<v-dialog
v-model="addSpeciesConfirmOpen"
max-width="400"
Expand Down Expand Up @@ -148,6 +215,51 @@ export default defineComponent({
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog
v-model="commentDialogOpen"
max-width="500"
persistent
>
<v-card>
<v-card-title>Comment</v-card-title>
<v-card-text>
<p class="text-medium-emphasis mb-3">
This is an optional comment field that can be attached to the annotation.
</p>
<v-textarea
v-model="commentDraft"
label="Comment"
rows="4"
variant="outlined"
auto-grow
/>
</v-card-text>
<v-card-actions>
<v-btn
v-if="commentDraft || annotationComment"
variant="text"
color="error"
@click="removeComment"
>
Remove comment
</v-btn>
<v-spacer />
<v-btn
variant="text"
@click="closeCommentDialog"
>
Cancel
</v-btn>
<v-btn
variant="flat"
color="primary"
@click="saveComment"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>

Expand Down