Skip to content

Commit cf700d5

Browse files
authored
Vetting progress UI (#293)
* Add settings for vetting * Model submission status of recording annotations * Add endpoint to get current user * Show recording submission status in table view * Don't filter out noise The model can predict noise, so sorting it out of the species list can lead to problems when loading/displaying that value in a list. * Add endpoint to submit file-level annotations * Allow submitting file annotations in interface * Squash migrations * Indicate when a file has been reviewed * Format * Show the current user's submitted label in sidebar * Disable deletion for non-admin vetters * Make 403 message more descriptive * Show progress bar for submitted recordings * Enable showing/hiding submitted recordings * Toggle submitted recordings in sidebar * Fix submission bug
1 parent a3feff3 commit cf700d5

File tree

4 files changed

+177
-37
lines changed

4 files changed

+177
-37
lines changed

bats_ai/core/views/recording_annotation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def submit_recording_annotation(request: HttpRequest, id: int):
201201
annotation = RecordingAnnotation.objects.get(pk=id)
202202

203203
# Check permission
204-
if annotation.recording.owner != request.user:
204+
if annotation.owner != request.user:
205205
raise HttpError(403, 'Permission denied.')
206206

207207
annotation.submitted = True

client/src/components/RecordingList.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import { EditingRecording } from './UploadRecording.vue';
77
export default defineComponent({
88
setup() {
99
10-
const { sharedList, recordingList, currentUser, configuration } = useState();
10+
const {
11+
sharedList,
12+
recordingList,
13+
currentUser,
14+
configuration,
15+
showSubmittedRecordings,
16+
myRecordingsDisplay,
17+
sharedRecordingsDisplay,
18+
} = useState();
1119
const editingRecording: Ref<EditingRecording | null> = ref(null);
1220
1321
const fetchRecordings = async () => {
@@ -47,18 +55,27 @@ export default defineComponent({
4755
openPanel,
4856
userSubmittedAnnotation,
4957
configuration,
58+
myRecordingsDisplay,
59+
sharedRecordingsDisplay,
60+
showSubmittedRecordings,
5061
};
5162
},
5263
});
5364
</script>
5465

5566
<template>
5667
<v-expansion-panels v-model="openPanel">
68+
<v-checkbox
69+
v-if="configuration.mark_annotations_completed_enabled"
70+
v-model="showSubmittedRecordings"
71+
label="Show submitted recordings"
72+
hide-details
73+
/>
5774
<v-expansion-panel>
5875
<v-expansion-panel-title>My Recordings</v-expansion-panel-title>
5976
<v-expansion-panel-text>
6077
<div
61-
v-for="item in recordingList"
78+
v-for="item in myRecordingsDisplay"
6279
:key="`public_${item.id}`"
6380
>
6481
<v-card class="pa-2 my-2">

client/src/use/useState.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ref, Ref, watch } from "vue";
1+
import { computed, ref, Ref, watch } from "vue";
22
import { useRouter } from 'vue-router';
33
import { cloneDeep } from "lodash";
44
import * as d3 from "d3";
@@ -158,6 +158,66 @@ export default function useState() {
158158
return router.currentRoute.value.fullPath.includes('nabat');
159159
}
160160

161+
const showSubmittedRecordings = ref(false);
162+
163+
const submittedMyRecordings = computed(() => {
164+
const submittedByMe = recordingList.value.filter((recording: Recording) => {
165+
const myAnnotations = recording.fileAnnotations.filter((annotation: FileAnnotation) => (
166+
annotation.owner === currentUser.value && annotation.submitted
167+
));
168+
return myAnnotations.length > 0;
169+
});
170+
return submittedByMe;
171+
});
172+
173+
const submittedSharedRecordings = computed(() => {
174+
const submittedByMe = sharedList.value.filter((recording: Recording) => {
175+
const myAnnotations = recording.fileAnnotations.filter((annotation: FileAnnotation) => (
176+
annotation.owner === currentUser.value && annotation.submitted
177+
));
178+
return myAnnotations.length > 0;
179+
});
180+
return submittedByMe;
181+
});
182+
183+
const unsubmittedMyRecordings = computed(() => {
184+
const unsubmitted = recordingList.value.filter((recording: Recording) => {
185+
const myAnnotations = recording.fileAnnotations.filter((annotation: FileAnnotation) => (
186+
annotation.owner === currentUser.value && annotation.submitted
187+
));
188+
return myAnnotations.length === 0;
189+
});
190+
return unsubmitted;
191+
});
192+
193+
const unsubmittedSharedRecordings = computed(() => {
194+
const unsubmitted = sharedList.value.filter((recording: Recording) => {
195+
const myAnnotations = recording.fileAnnotations.filter((annotation: FileAnnotation) => (
196+
annotation.owner === currentUser.value && annotation.submitted
197+
));
198+
return myAnnotations.length === 0;
199+
});
200+
return unsubmitted;
201+
});
202+
203+
// Use state to determine which recordings should be shown to the user
204+
const myRecordingsDisplay = computed(() => {
205+
if (!configuration.value.mark_annotations_completed_enabled) {
206+
return recordingList.value;
207+
} else {
208+
return showSubmittedRecordings.value ? recordingList.value : unsubmittedMyRecordings.value;
209+
}
210+
});
211+
212+
const sharedRecordingsDisplay = computed(() => {
213+
if (!configuration.value.mark_annotations_completed_enabled) {
214+
return sharedList.value;
215+
} else {
216+
return showSubmittedRecordings.value ? sharedList.value : unsubmittedSharedRecordings.value;
217+
}
218+
});
219+
220+
161221

162222
return {
163223
annotationState,
@@ -201,5 +261,12 @@ export default function useState() {
201261
scaledHeight,
202262
fixedAxes,
203263
toggleFixedAxes,
264+
showSubmittedRecordings,
265+
submittedMyRecordings,
266+
submittedSharedRecordings,
267+
unsubmittedMyRecordings,
268+
unsubmittedSharedRecordings,
269+
myRecordingsDisplay,
270+
sharedRecordingsDisplay,
204271
};
205272
}

client/src/views/Recordings.vue

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import RecordingAnnotationSummary from '@components/RecordingAnnotationSummary.v
2222
import { FilterFunction, InternalItem } from 'vuetify';
2323
2424
export default defineComponent({
25-
components: {
26-
UploadRecording,
27-
MapLocation,
28-
BatchUploadRecording,
29-
RecordingInfoDisplay,
30-
RecordingAnnotationSummary,
31-
},
25+
components: {
26+
UploadRecording,
27+
MapLocation,
28+
BatchUploadRecording,
29+
RecordingInfoDisplay,
30+
RecordingAnnotationSummary,
31+
},
3232
setup() {
3333
const itemsPerPage = ref(-1);
3434
const {
@@ -38,6 +38,11 @@ export default defineComponent({
3838
currentUser,
3939
configuration,
4040
loadCurrentUser,
41+
showSubmittedRecordings,
42+
submittedMyRecordings,
43+
submittedSharedRecordings,
44+
myRecordingsDisplay,
45+
sharedRecordingsDisplay,
4146
} = useState();
4247
const editingRecording: Ref<EditingRecording | null> = ref(null);
4348
let intervalRef: number | null = null;
@@ -231,6 +236,14 @@ export default defineComponent({
231236
}
232237
}
233238
239+
const recordingListStyles = computed(() => {
240+
const sectionHeight = configuration.value.mark_annotations_completed_enabled ? '35vh' : '40vh';
241+
return {
242+
'height': sectionHeight,
243+
'max-height': sectionHeight,
244+
};
245+
});
246+
234247
onMounted(async () => {
235248
await loadCurrentUser();
236249
await fetchRecordingTags();
@@ -308,6 +321,12 @@ export default defineComponent({
308321
dataLoading,
309322
submittedForCurrentUser,
310323
configuration,
324+
submittedMyRecordings,
325+
submittedSharedRecordings,
326+
recordingListStyles,
327+
showSubmittedRecordings,
328+
myRecordingsDisplay,
329+
sharedRecordingsDisplay,
311330
};
312331
},
313332
});
@@ -321,26 +340,35 @@ export default defineComponent({
321340
My Recordings
322341
</div>
323342
<v-spacer />
324-
<v-menu>
325-
<template #activator="{ props }">
326-
<v-btn
327-
color="primary"
328-
v-bind="props"
329-
>
330-
Upload <v-icon>mdi-chevron-down</v-icon>
331-
</v-btn>
332-
</template>
333-
<v-list>
334-
<v-list-item @click="uploadDialog=true">
335-
<v-list-item-title>Upload Recording</v-list-item-title>
336-
</v-list-item>
337-
<v-list-item @click="batchUploadDialog=true">
338-
<v-list-item-title>
339-
Batch Upload
340-
</v-list-item-title>
341-
</v-list-item>
342-
</v-list>
343-
</v-menu>
343+
<div class="d-flex justify-center align-center">
344+
<v-checkbox
345+
v-if="configuration.mark_annotations_completed_enabled"
346+
v-model="showSubmittedRecordings"
347+
class="mr-4"
348+
label="Show submitted recordings"
349+
hide-details
350+
/>
351+
<v-menu>
352+
<template #activator="{ props }">
353+
<v-btn
354+
color="primary"
355+
v-bind="props"
356+
>
357+
Upload <v-icon>mdi-chevron-down</v-icon>
358+
</v-btn>
359+
</template>
360+
<v-list>
361+
<v-list-item @click="uploadDialog=true">
362+
<v-list-item-title>Upload Recording</v-list-item-title>
363+
</v-list-item>
364+
<v-list-item @click="batchUploadDialog=true">
365+
<v-list-item-title>
366+
Batch Upload
367+
</v-list-item-title>
368+
</v-list-item>
369+
</v-list>
370+
</v-menu>
371+
</div>
344372
</v-row>
345373
</v-card-title>
346374
<v-card-text>
@@ -372,13 +400,14 @@ export default defineComponent({
372400
<v-data-table
373401
v-model:items-per-page="itemsPerPage"
374402
:headers="headers"
375-
:items="recordingList"
403+
:items="myRecordingsDisplay"
376404
:custom-filter="tagFilter"
377405
filter-keys="['tag']"
378406
:search="filterTags.length ? 'seach-active' : ''"
379407
density="compact"
380408
:loading="dataLoading"
381409
class="elevation-1 my-recordings"
410+
:style="recordingListStyles"
382411
>
383412
<template #top>
384413
<div max-height="100px">
@@ -524,6 +553,21 @@ export default defineComponent({
524553
</template>
525554
<template #bottom />
526555
</v-data-table>
556+
<div
557+
v-if="recordingList.length && configuration.mark_annotations_completed_enabled"
558+
class="d-flex justify-center align-center"
559+
>
560+
<v-progress-linear
561+
height="10"
562+
color="success"
563+
:model-value="submittedMyRecordings.length"
564+
min="0"
565+
:max="recordingList.length"
566+
/>
567+
<span class="ml-4 text-h6">
568+
({{ submittedMyRecordings.length }}/{{ recordingList.length }})
569+
</span>
570+
</div>
527571
</v-card-text>
528572
<v-dialog
529573
v-model="uploadDialog"
@@ -557,13 +601,14 @@ export default defineComponent({
557601
<v-data-table
558602
v-model:items-per-page="itemsPerPage"
559603
:headers="sharedHeaders"
560-
:items="sharedList"
604+
:items="sharedRecordingsDisplay"
561605
:custom-filter="sharedTagFilter"
562606
filter-keys="['tag']"
563607
:search="sharedFilterTags.length ? 'seach-active' : ''"
564608
:loading="dataLoading"
565609
density="compact"
566610
class="elevation-1 shared-recordings"
611+
:style="recordingListStyles"
567612
>
568613
<template #top>
569614
<div max-height="100px">
@@ -683,19 +728,30 @@ export default defineComponent({
683728
</template>
684729
<template #bottom />
685730
</v-data-table>
731+
<div
732+
v-if="sharedList.length && configuration.mark_annotations_completed_enabled"
733+
class="d-flex justify-center align-center"
734+
>
735+
<v-progress-linear
736+
height="10"
737+
color="success"
738+
:model-value="submittedSharedRecordings.length"
739+
min="0"
740+
:max="sharedList.length"
741+
/>
742+
<span class="ml-4 text-h6">
743+
({{ submittedSharedRecordings.length }}/{{ sharedList.length }})
744+
</span>
745+
</div>
686746
</v-card-text>
687747
</v-card>
688748
</template>
689749

690750
<style scoped>
691751
.my-recordings {
692-
height: 40vh;
693-
max-height: 40vh;
694752
overflow-y:scroll;
695753
}
696754
.shared-recordings {
697-
height: 40vh;
698-
max-height: 40vh;
699755
overflow-y:scroll;
700756
}
701757
</style>

0 commit comments

Comments
 (0)