Skip to content

Commit

Permalink
Server Feedback Updates (#44)
Browse files Browse the repository at this point in the history
* update for spectro creation, update pulse/sequence, add delete

* filter from public files without spectrograms

* refactoring login
  • Loading branch information
BryonLewis authored Feb 21, 2024
1 parent 3b582ae commit 9ad5f94
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 25 deletions.
4 changes: 2 additions & 2 deletions INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ that area in the main view.
On the right side of the screen is a list of the Sequence and Pulse annotations.
These are tabbed views that can be switched between by clicking on the Tab Name.
Annotations can be selected by clicking on them.
When an annotation is selected it can be edited including the species
comments and other information.
When an annotation is selected it can be edited including the
species comments and other information.

**Sequence** - A Sequence annotation is typically used to group multiple pulses together
This can be drawn by clicking on the 'Add+' button and drawing a box around the pulses.
Expand Down
37 changes: 34 additions & 3 deletions bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
from django.contrib.auth.models import User
from django.contrib.gis.geos import Point
from django.core.files.storage import default_storage
from django.db.models import Q
from django.http import HttpRequest
from ninja import File, Form, Schema
from ninja.files import UploadedFile
from ninja.pagination import RouterPaginated

from bats_ai.core.models import Annotations, Recording, Species, Spectrogram, TemporalAnnotations
from bats_ai.core.models import Annotations, Recording, Species, TemporalAnnotations
from bats_ai.core.tasks import recording_compute_spectrogram
from bats_ai.core.views.species import SpeciesSchema
from bats_ai.core.views.temporal_annotations import (
TemporalAnnotationSchema,
Expand Down Expand Up @@ -106,7 +108,7 @@ def create_recording(
# Start generating recording as soon as created
# this creates the spectrogram during the upload so it is available immediately afterwards
# it will make the upload process longer but I think it's worth it.
Spectrogram.generate(recording)
recording_compute_spectrogram.delay(recording.pk)
return {'message': 'Recording updated successfully', 'id': recording.pk}


Expand Down Expand Up @@ -137,11 +139,39 @@ def update_recording(request: HttpRequest, id: int, recording_data: RecordingUpl
return {'message': 'Recording updated successfully', 'id': recording.pk}


@router.delete('/{id}')
def delete_recording(
request,
id: int,
):
try:
recording = Recording.objects.get(pk=id)

# Check if the user owns the recording or if the recording is public
if recording.owner == request.user or recording.public:
# Delete the annotation
recording.delete()
return {'message': 'Recording deleted successfully'}
else:
return {
'error': 'Permission denied. You do not own this recording, and it is not public.'
}

except Recording.DoesNotExist:
return {'error': 'Recording not found'}
except Annotations.DoesNotExist:
return {'error': 'Annotation not found'}


@router.get('/')
def get_recordings(request: HttpRequest, public: bool | None = None):
# Filter recordings based on the owner's id or public=True
if public is not None and public:
recordings = Recording.objects.filter(public=True).exclude(owner=request.user).values()
recordings = (
Recording.objects.filter(public=True)
.exclude(Q(owner=request.user) | Q(spectrogram__isnull=True))
.values()
)
else:
recordings = Recording.objects.filter(owner=request.user).values()

Expand All @@ -150,6 +180,7 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
user = User.objects.get(id=recording['owner_id'])
recording['owner_username'] = user.username
recording['audio_file_presigned_url'] = default_storage.url(recording['audio_file'])
recording['hasSpectrogram'] = Recording.objects.get(id=recording['id']).has_spectrogram
if recording['recording_location']:
recording['recording_location'] = json.loads(recording['recording_location'].json)
unique_users_with_annotations = (
Expand Down
30 changes: 20 additions & 10 deletions client/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
<script lang="ts">
import { defineComponent, inject, computed, ref } from 'vue';
import OAuthClient from '@girder/oauth-client';
import { useRouter } from 'vue-router';
import { defineComponent, inject, ref, onMounted } from "vue";
import OAuthClient from "@girder/oauth-client";
import { useRouter } from "vue-router";
export default defineComponent({
setup() {
const oauthClient = inject<OAuthClient>('oauthClient');
const oauthClient = inject<OAuthClient>("oauthClient");
const router = useRouter();
if (oauthClient === undefined) {
throw new Error('Must provide "oauthClient" into component.');
}
const loginText = computed(() => (oauthClient.isLoggedIn ? 'Logout' : 'Login'));
const loginText = ref('Login');
const checkLogin = () => {
if (oauthClient.isLoggedIn) {
loginText.value = "Logout";
} else {
loginText.value = "Login";
}
};
const logInOrOut = async () => {
if (oauthClient.isLoggedIn) {
await oauthClient.logout();
router.push('Login');
localStorage.clear();
router.push("Login");
checkLogin();
} else {
oauthClient.redirectToLogin();
}
};
const activeTab = ref('recordings');
onMounted(() => {
checkLogin();
});
const activeTab = ref("recordings");
return { oauthClient, loginText, logInOrOut, activeTab };
},
});
Expand Down Expand Up @@ -48,9 +60,7 @@ export default defineComponent({
</v-tab>
</v-tabs>
<v-spacer />
<v-btn
@click="logInOrOut"
>
<v-btn @click="logInOrOut">
{{ loginText }}
</v-btn>
</v-app-bar>
Expand Down
18 changes: 15 additions & 3 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Recording {
public: boolean;
userMadeAnnotations: boolean;
userAnnotations: number;
hasSpectrogram: boolean;
}

export interface AcousticFiles {
Expand Down Expand Up @@ -181,11 +182,21 @@ async function uploadRecordingFile(file: File, name: string, recorded_date: stri
}
);
}

interface DeletionResponse {
message?: string;
error?: string;
}

async function getRecordings(getPublic=false) {
return axiosInstance.get<Recording[]>(`/recording?public=${getPublic}`);
return axiosInstance.get<Recording[]>(`/recording/?public=${getPublic}`);
}

async function deleteRecording(id: number) {
return axiosInstance.delete<DeletionResponse>(`/recording/${id}`);
}


async function getSpectrogram(id: string) {
return axiosInstance.get<Spectrogram>(`/recording/${id}/spectrogram`);
}
Expand Down Expand Up @@ -224,11 +235,11 @@ async function putTemporalAnnotation(recordingId: string, annotation: UpdateSpec
}

async function deleteAnnotation(recordingId: string, annotationId: number) {
return axiosInstance.delete(`/recording/${recordingId}/annotations/${annotationId}`);
return axiosInstance.delete<DeletionResponse>(`/recording/${recordingId}/annotations/${annotationId}`);
}

async function deleteTemporalAnnotation(recordingId: string, annotationId: number) {
return axiosInstance.delete(`/recording/${recordingId}/temporal-annotations/${annotationId}`);
return axiosInstance.delete<DeletionResponse>(`/recording/${recordingId}/temporal-annotations/${annotationId}`);
}

async function getOtherUserAnnotations(recordingId: string) {
Expand All @@ -239,6 +250,7 @@ export {
uploadRecordingFile,
getRecordings,
patchRecording,
deleteRecording,
getSpectrogram,
getSpectrogramCompressed,
getTemporalAnnotations,
Expand Down
22 changes: 17 additions & 5 deletions client/src/components/AnnotationList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,29 @@ export default defineComponent({
emits: ['select'],
setup() {
const { creationType, annotationState, setAnnotationState, annotations, temporalAnnotations, selectedId, selectedType, setSelectedId } = useState();
const tab = ref('sequence');
const tab = ref('pulse');
const scrollToId = (id: number) => {
const el = document.getElementById(`annotation-${id}`);
if (el) {
el.scrollIntoView({block: 'end', behavior: 'smooth'});
}
};
watch(selectedId, () => {
tab.value = selectedType.value;
if (selectedId.value !== null) {
scrollToId(selectedId.value);
}
});
watch(selectedType, () => {
tab.value = selectedType.value;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tabSwitch = (event: any) => {
// On tab switches we want to deselect the curret anntation
tab.value = event as 'sequence' | 'pulse';
selectedType.value = event as 'sequence' | 'pulse';
selectedId.value = null;
};
return {
annotationState,
Expand All @@ -42,6 +50,7 @@ export default defineComponent({
selectedType,
setAnnotationState,
setSelectedId,
tabSwitch,
tab,
};
},
Expand All @@ -51,13 +60,16 @@ export default defineComponent({
<template>
<v-card class="pa-0 ma-0">
<v-card-title>
<v-tabs v-model="tab">
<v-tab value="sequence">
Sequence
</v-tab>
<v-tabs
v-model="tab"
@update:model-value="tabSwitch($event)"
>
<v-tab value="pulse">
Pulse
</v-tab>
<v-tab value="sequence">
Sequence
</v-tab>
</v-tabs>
</v-card-title>
<v-card-text class="">
Expand Down
2 changes: 1 addition & 1 deletion client/src/use/useState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const colorScale: Ref<d3.ScaleOrdinal<string, string, never> | undefined> = ref(
const selectedUsers: Ref<string[]> = ref([]);
const currentUser: Ref<string> = ref('');
const selectedId: Ref<number | null> = ref(null);
const selectedType: Ref<'pulse' | 'sequence'> = ref('sequence');
const selectedType: Ref<'pulse' | 'sequence'> = ref('pulse');
const annotations : Ref<SpectrogramAnnotation[]> = ref([]);
const temporalAnnotations: Ref<SpectrogramTemporalAnnotation[]> = ref([]);
const otherUserAnnotations: Ref<OtherUserAnnotations> = ref({});
Expand Down
44 changes: 43 additions & 1 deletion client/src/views/Recordings.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { defineComponent, ref, Ref, onMounted } from 'vue';
import { getRecordings, Recording } from '../api/api';
import { deleteRecording, getRecordings, Recording } from '../api/api';
import {
VDataTable,
} from "vuetify/labs/VDataTable";
Expand All @@ -18,6 +18,7 @@ export default defineComponent({
const recordingList: Ref<Recording[]> = ref([]);
const sharedList: Ref<Recording[]> = ref([]);
const editingRecording: Ref<EditingRecording | null> = ref(null);
let intervalRef: number | null = null;
const uploadDialog = ref(false);
const headers = ref([
Expand Down Expand Up @@ -97,6 +98,23 @@ export default defineComponent({
const fetchRecordings = async () => {
const recordings = await getRecordings();
recordingList.value = recordings.data;
// If we have a spectrogram being generated we need to refresh on an interval
let missingSpectro = false;
for (let i =0; i< recordingList.value.length; i+=1) {
if (!recordingList.value[i].hasSpectrogram) {
missingSpectro = true;
break;
}
}
if (missingSpectro) {
if (intervalRef === null) {
intervalRef = setInterval(() => fetchRecordings(), 5000);
}
} else {
if (intervalRef !== null) {
clearInterval(intervalRef);
}
}
const shared = await getRecordings(true);
sharedList.value = shared.data;
Expand Down Expand Up @@ -124,6 +142,10 @@ export default defineComponent({
}
uploadDialog.value = true;
};
const delRecording = async (id: number) => {
await deleteRecording(id);
fetchRecordings();
};
return {
itemsPerPage,
Expand All @@ -134,6 +156,7 @@ export default defineComponent({
uploadDialog,
uploadDone,
editRecording,
delRecording,
editingRecording,
};
},
Expand Down Expand Up @@ -168,14 +191,33 @@ export default defineComponent({
<v-icon @click="editRecording(item.raw)">
mdi-pencil
</v-icon>
<v-icon
color="error"
@click="delRecording(item.raw.id)"
>
mdi-delete
</v-icon>
</template>

<template #item.name="{ item }">
<router-link
v-if="item.raw.hasSpectrogram"
:to="`/recording/${item.raw.id.toString()}/spectrogram`"
>
{{ item.raw.name }}
</router-link>
<div v-else>
{{ item.raw.name }}
<v-tooltip bottom>
<template #activator="{ props: subProps }">
<span v-bind="subProps">
<v-icon color="warning">mdi-alert</v-icon>
<v-icon>mdi-sync mdi-spin</v-icon>
</span>
</template>
<span>Waiting for spectrogram to be computed</span>
</v-tooltip>
</div>
</template>
<template #item.recording_location="{ item }">
<v-menu
Expand Down

0 comments on commit 9ad5f94

Please sign in to comment.