diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d9f5dc6..93dfa44 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ env: jobs: lint-python: name: Lint Python - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -55,7 +55,7 @@ jobs: working-directory: client test-django: name: Test Django [${{ matrix.tox-env }}] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: @@ -92,7 +92,7 @@ jobs: - name: Update Package References run: sudo apt-get update - name: Install system dependencies - run: apt-fast install --no-install-recommends --yes + run: sudo apt-get install --no-install-recommends --yes libgdal30 libproj22 python3-cachecontrol @@ -110,7 +110,7 @@ jobs: working-directory: bats_ai test-vue: name: Test [vue] - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/bats_ai/api.py b/bats_ai/api.py index 88ab5cd..87a6469 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -3,7 +3,13 @@ from ninja import NinjaAPI from oauth2_provider.models import AccessToken -from bats_ai.core.views import GRTSCellsRouter, GuanoMetadataRouter, RecordingRouter, SpeciesRouter +from bats_ai.core.views import ( + GRTSCellsRouter, + GuanoMetadataRouter, + RecordingAnnotationRouter, + RecordingRouter, + SpeciesRouter, +) logger = logging.getLogger(__name__) @@ -28,3 +34,4 @@ def global_auth(request): api.add_router('/species/', SpeciesRouter) api.add_router('/grts/', GRTSCellsRouter) api.add_router('/guano/', GuanoMetadataRouter) +api.add_router('/recording-annotation/', RecordingAnnotationRouter) diff --git a/bats_ai/core/migrations/0011_alter_annotations_options_annotations_confidence_and_more.py b/bats_ai/core/migrations/0011_alter_annotations_options_annotations_confidence_and_more.py new file mode 100644 index 0000000..4269fb1 --- /dev/null +++ b/bats_ai/core/migrations/0011_alter_annotations_options_annotations_confidence_and_more.py @@ -0,0 +1,107 @@ +# Generated by Django 4.1.13 on 2024-12-09 15:26 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('core', '0010_compressedspectrogram'), + ] + + operations = [ + migrations.AlterModelOptions( + name='annotations', + options={'get_latest_by': 'modified'}, + ), + migrations.AddField( + model_name='annotations', + name='confidence', + field=models.FloatField( + default=1.0, + help_text='A confidence value between 0 and 1.0, default is 1.0.', + validators=[ + django.core.validators.MinValueValidator(0.0), + django.core.validators.MaxValueValidator(1.0), + ], + ), + ), + migrations.AddField( + model_name='annotations', + name='created', + field=django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, default=django.utils.timezone.now, verbose_name='created' + ), + preserve_default=False, + ), + migrations.AddField( + model_name='annotations', + name='model', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='annotations', + name='modified', + field=django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name='modified' + ), + ), + migrations.CreateModel( + name='RecordingAnnotation', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ( + 'created', + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name='created' + ), + ), + ( + 'modified', + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name='modified' + ), + ), + ('comments', models.TextField(blank=True, null=True)), + ('model', models.TextField(blank=True, null=True)), + ( + 'confidence', + models.FloatField( + default=1.0, + help_text='A confidence value between 0 and 1.0, default is 1.0.', + validators=[ + django.core.validators.MinValueValidator(0.0), + django.core.validators.MaxValueValidator(1.0), + ], + ), + ), + ( + 'owner', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ( + 'recording', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='core.recording' + ), + ), + ('species', models.ManyToManyField(to='core.species')), + ], + options={ + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index 36e2f0f..f53dc0b 100644 --- a/bats_ai/core/models/__init__.py +++ b/bats_ai/core/models/__init__.py @@ -3,6 +3,7 @@ from .grts_cells import GRTSCells from .image import Image from .recording import Recording, colormap +from .recording_annotation import RecordingAnnotation from .recording_annotation_status import RecordingAnnotationStatus from .species import Species from .spectrogram import Spectrogram @@ -19,4 +20,5 @@ 'GRTSCells', 'colormap', 'CompressedSpectrogram', + 'RecordingAnnotation', ] diff --git a/bats_ai/core/models/annotations.py b/bats_ai/core/models/annotations.py index 634c6b3..6028ffb 100644 --- a/bats_ai/core/models/annotations.py +++ b/bats_ai/core/models/annotations.py @@ -1,11 +1,13 @@ from django.contrib.auth.models import User +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django_extensions.db.models import TimeStampedModel from .recording import Recording from .species import Species -class Annotations(models.Model): +class Annotations(TimeStampedModel, models.Model): recording = models.ForeignKey(Recording, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE) start_time = models.IntegerField(blank=True, null=True) @@ -15,3 +17,12 @@ class Annotations(models.Model): type = models.TextField(blank=True, null=True) species = models.ManyToManyField(Species) comments = models.TextField(blank=True, null=True) + model = models.TextField(blank=True, null=True) # AI Model information if inference used + confidence = models.FloatField( + default=1.0, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0), + ], + help_text='A confidence value between 0 and 1.0, default is 1.0.', + ) diff --git a/bats_ai/core/models/recording_annotation.py b/bats_ai/core/models/recording_annotation.py new file mode 100644 index 0000000..3429560 --- /dev/null +++ b/bats_ai/core/models/recording_annotation.py @@ -0,0 +1,23 @@ +from django.contrib.auth.models import User +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django_extensions.db.models import TimeStampedModel + +from .recording import Recording +from .species import Species + + +class RecordingAnnotation(TimeStampedModel, models.Model): + recording = models.ForeignKey(Recording, on_delete=models.CASCADE) + 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 + confidence = models.FloatField( + default=1.0, + validators=[ + MinValueValidator(0.0), + MaxValueValidator(1.0), + ], + help_text='A confidence value between 0 and 1.0, default is 1.0.', + ) diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index 90163e3..348849c 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -2,6 +2,7 @@ from .grts_cells import router as GRTSCellsRouter from .guanometadata import router as GuanoMetadataRouter from .recording import router as RecordingRouter +from .recording_annotation import router as RecordingAnnotationRouter from .species import router as SpeciesRouter from .temporal_annotations import router as TemporalAnnotationRouter @@ -12,4 +13,5 @@ 'TemporalAnnotationRouter', 'GRTSCellsRouter', 'GuanoMetadataRouter', + 'RecordingAnnotationRouter', ] diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 7699cc8..c935880 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -15,6 +15,7 @@ Annotations, CompressedSpectrogram, Recording, + RecordingAnnotation, Species, TemporalAnnotations, colormap, @@ -61,6 +62,26 @@ class RecordingUploadSchema(Schema): unusual_occurrences: str = None +class RecordingAnnotationSchema(Schema): + species: list[SpeciesSchema] | None + comments: str | None = None + model: str | None = None + owner: str + confidence: float + id: int | None = None + + @classmethod + def from_orm(cls, obj: RecordingAnnotation, **kwargs): + return cls( + species=[SpeciesSchema.from_orm(species) for species in obj.species.all()], + owner=obj.owner.username, + confidence=obj.confidence, + comments=obj.comments, + model=obj.model, + id=obj.pk, + ) + + class AnnotationSchema(Schema): start_time: int end_time: int @@ -73,7 +94,7 @@ class AnnotationSchema(Schema): owner_email: str = None @classmethod - def from_orm(cls, obj, owner_email=None, **kwargs): + def from_orm(cls, obj: Annotations, owner_email=None, **kwargs): return cls( start_time=obj.start_time, end_time=obj.end_time, @@ -215,6 +236,11 @@ def get_recordings(request: HttpRequest, public: bool | None = None): # TODO with larger dataset it may be better to do this in a queryset instead of python for recording in recordings: user = User.objects.get(id=recording['owner_id']) + fileAnnotations = RecordingAnnotation.objects.filter(recording=recording['id']) + recording['fileAnnotations'] = [ + RecordingAnnotationSchema.from_orm(fileAnnotation).dict() + for fileAnnotation in fileAnnotations + ] 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 @@ -227,9 +253,12 @@ def get_recordings(request: HttpRequest, public: bool | None = None): .count() ) recording['userAnnotations'] = unique_users_with_annotations - user_has_annotations = Annotations.objects.filter( - recording_id=recording['id'], owner=request.user - ).exists() + user_has_annotations = ( + Annotations.objects.filter(recording_id=recording['id'], owner=request.user).exists() + or RecordingAnnotation.objects.filter( + recording_id=recording['id'], owner=request.user + ).exists() + ) recording['userMadeAnnotations'] = user_has_annotations return list(recordings) @@ -249,17 +278,38 @@ def get_recording(request: HttpRequest, id: int): 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 = ( + annotation_owners = ( Annotations.objects.filter(recording_id=recording['id']) - .values('owner') + .values_list('owner', flat=True) .distinct() - .count() + ) + recording_annotation_owners = ( + RecordingAnnotation.objects.filter(recording_id=recording['id']) + .values_list('owner', flat=True) + .distinct() + ) + + # Combine the sets of owners and count unique entries + unique_users_with_annotations = len( + set(annotation_owners).union(set(recording_annotation_owners)) ) recording['userAnnotations'] = unique_users_with_annotations - user_has_annotations = Annotations.objects.filter( - recording_id=recording['id'], owner=request.user - ).exists() + user_has_annotations = ( + Annotations.objects.filter( + recording_id=recording['id'], owner=request.user + ).exists() + or RecordingAnnotation.objects.filter( + recording_id=recording['id'], owner=request.user + ).exists() + ) recording['userMadeAnnotations'] = user_has_annotations + fileAnnotations = RecordingAnnotation.objects.filter(recording=id).order_by( + 'confidence' + ) + recording['fileAnnotations'] = [ + RecordingAnnotationSchema.from_orm(fileAnnotation).dict() + for fileAnnotation in fileAnnotations + ] return recording else: @@ -268,6 +318,18 @@ def get_recording(request: HttpRequest, id: int): return {'error': 'Recording not found'} +@router.get('/{recording_id}/recording-annotations') +def get_recording_annotations(request: HttpRequest, recording_id: int): + fileAnnotations = RecordingAnnotation.objects.filter(recording=recording_id).order_by( + 'confidence' + ) + output = [ + RecordingAnnotationSchema.from_orm(fileAnnotation).dict() + for fileAnnotation in fileAnnotations + ] + return output + + @router.get('/{id}/spectrogram') def get_spectrogram(request: HttpRequest, id: int): try: diff --git a/bats_ai/core/views/recording_annotation.py b/bats_ai/core/views/recording_annotation.py new file mode 100644 index 0000000..20fc06f --- /dev/null +++ b/bats_ai/core/views/recording_annotation.py @@ -0,0 +1,140 @@ +import logging + +from django.http import HttpRequest +from ninja import Router, Schema +from ninja.errors import HttpError + +from bats_ai.core.models import Recording, RecordingAnnotation, Species +from bats_ai.core.views.recording import SpeciesSchema + +logger = logging.getLogger(__name__) + +router = Router() + + +# Schemas for serialization +class RecordingAnnotationSchema(Schema): + species: list[SpeciesSchema] | None + comments: str | None = None + model: str | None = None + owner: str + confidence: float + id: int | None = None + + @classmethod + def from_orm(cls, obj: RecordingAnnotation, **kwargs): + return cls( + species=[SpeciesSchema.from_orm(species) for species in obj.species.all()], + owner=obj.owner.username, + confidence=obj.confidence, + comments=obj.comments, + model=obj.model, + id=obj.pk, + ) + + +class CreateRecordingAnnotationSchema(Schema): + recordingId: int + species: list[int] + comments: str = None + model: str = None + confidence: float + + +class UpdateRecordingAnnotationSchema(Schema): + species: list[int] = None + comments: str = None + model: str = None + confidence: float = None + + +@router.get('/{id}', response=RecordingAnnotationSchema) +def get_recording_annotation(request: HttpRequest, id: int): + try: + annotation = RecordingAnnotation.objects.get(pk=id) + + # Check permission + if annotation.recording.owner != request.user and not annotation.recording.public: + raise HttpError(403, 'Permission denied.') + + return RecordingAnnotationSchema.from_orm(annotation).dict() + except RecordingAnnotation.DoesNotExist: + raise HttpError(404, 'Recording annotation not found.') + + +@router.put('/', response={200: str}) +def create_recording_annotation(request: HttpRequest, data: CreateRecordingAnnotationSchema): + try: + recording = Recording.objects.get(pk=data.recordingId) + + # Check permission + if recording.owner != request.user and not recording.public: + raise HttpError(403, 'Permission denied.') + + # Create the recording annotation + annotation = RecordingAnnotation.objects.create( + recording=recording, + owner=request.user, + comments=data.comments, + model=data.model, + confidence=data.confidence, + ) + + # Add species + for species_id in data.species: + species = Species.objects.get(pk=species_id) + annotation.species.add(species) + + return 'Recording annotation created successfully.' + except Recording.DoesNotExist: + raise HttpError(404, 'Recording not found.') + except Species.DoesNotExist: + raise HttpError(404, 'One or more species IDs not found.') + + +@router.patch('/{id}', response={200: str}) +def update_recording_annotation( + request: HttpRequest, id: int, data: UpdateRecordingAnnotationSchema +): + try: + annotation = RecordingAnnotation.objects.get(pk=id) + + # Check permission + if annotation.recording.owner != request.user and not annotation.recording.public: + raise HttpError(403, 'Permission denied.') + + # Update fields if provided + if data.comments is not None: + annotation.comments = data.comments + if data.model is not None: + annotation.model = data.model + if data.confidence is not None: + annotation.confidence = data.confidence + if data.species is not None: + annotation.species.clear() # Clear existing species + for species_id in data.species: + species = Species.objects.get(pk=species_id) + annotation.species.add(species) + + annotation.save() + return 'Recording annotation updated successfully.' + except RecordingAnnotation.DoesNotExist: + raise HttpError(404, 'Recording annotation not found.') + except Species.DoesNotExist: + raise HttpError(404, 'One or more species IDs not found.') + + +# DELETE Endpoint +@router.delete('/{id}', response={200: str}) +def delete_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.delete() + return 'Recording annotation deleted successfully.' + except RecordingAnnotation.DoesNotExist: + raise HttpError(404, 'Recording annotation not found.') diff --git a/client/package.json b/client/package.json index 7237695..ef94d4f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "vue-project-template", + "name": "bat-ai-client", "version": "0.1.0", "private": true, "scripts": { diff --git a/client/src/App.vue b/client/src/App.vue index c2fb841..df706fa 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -12,7 +12,7 @@ export default defineComponent({ const oauthClient = inject("oauthClient"); const router = useRouter(); const route = useRoute(); - const { nextShared, sharedList } = useState(); + const { nextShared, sharedList, sideTab, } = useState(); const getShared = async () => { sharedList.value = (await getRecordings(true)).data; }; @@ -57,7 +57,7 @@ export default defineComponent({ } }); - return { oauthClient, containsSpectro, loginText, logInOrOut, activeTab, nextShared }; + return { oauthClient, containsSpectro, loginText, logInOrOut, activeTab, nextShared, sideTab }; }, }); diff --git a/client/src/api/api.ts b/client/src/api/api.ts index bb9f824..cbd5817 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -28,6 +28,7 @@ export interface Recording { userMadeAnnotations: boolean; userAnnotations: number; hasSpectrogram: boolean; + fileAnnotations: FileAnnotation[]; site_name?: string; software?: string; detector?: string; @@ -107,10 +108,30 @@ export interface UserInfo { email: string; id: number; } + +export interface FileAnnotation { + species: Species[]; + comments?: string; + model?: string; + owner: string; + confidence: number; + id: number; +} + +export interface UpdateFileAnnotation { + recordingId?: number; + species: number[] | null; + comments?: string; + model?: string; + confidence: number; + id?: number; +} + export interface Spectrogram { url: string; filename?: string; annotations?: SpectrogramAnnotation[]; + fileAnnotations: FileAnnotation[]; temporal?: SpectrogramTemporalAnnotation[]; spectroInfo?: SpectroInfo; compressed?: { @@ -310,6 +331,23 @@ async function getOtherUserAnnotations(recordingId: string) { async function getCellLocation(cellId: number, quadrant?: 'SW' | 'NE' | 'NW' | 'SE') { return axiosInstance.get(`/grts/${cellId}`, { params: { quadrant }}); } +async function getFileAnnotations(recordingId: number) { + return axiosInstance.get(`recording/${recordingId}/recording-annotations`); +} + + +async function putFileAnnotation(fileAnnotation: UpdateFileAnnotation) { + return axiosInstance.put<{message: string, id: number}>(`/recording-annotation/`, { ...fileAnnotation }); +} + +async function patchFileAnnotation(fileAnnotationId: number, fileAnnotation: UpdateFileAnnotation) { + return axiosInstance.patch<{message: string, id: number}>(`/recording-annotation/${fileAnnotationId}`, { ...fileAnnotation }); +} + +async function deleteFileAnnotation(fileAnnotationId: number) { + return axiosInstance.delete<{message: string, id: number}>(`/recording-annotation/${fileAnnotationId}`); +} + interface CellIDReponse { grid_cell_id?: number; @@ -367,4 +405,8 @@ export { getCellLocation, getCellfromLocation, getGuanoMetadata, + getFileAnnotations, + putFileAnnotation, + patchFileAnnotation, + deleteFileAnnotation, }; \ No newline at end of file diff --git a/client/src/components/AnnotationEditor.vue b/client/src/components/AnnotationEditor.vue index 40cc2a3..f516fae 100644 --- a/client/src/components/AnnotationEditor.vue +++ b/client/src/components/AnnotationEditor.vue @@ -115,43 +115,45 @@ export default defineComponent({ - - - - - - - - - + + + + + + + + + + - - - + + + + diff --git a/client/src/components/AnnotationList.vue b/client/src/components/AnnotationList.vue index 6832295..d06034b 100644 --- a/client/src/components/AnnotationList.vue +++ b/client/src/components/AnnotationList.vue @@ -3,23 +3,37 @@ import { defineComponent, PropType } from "vue"; import { SpectroInfo } from './geoJS/geoJSUtils'; import useState from "../use/useState"; import { watch, ref } from "vue"; -import RecordingList from "./RecordingList.vue"; - +import AnnotationEditor from "./AnnotationEditor.vue"; +import { Species, SpectrogramAnnotation, SpectrogramTemporalAnnotation } from "../api/api"; +import RecordingAnnotations from "./RecordingAnnotations.vue"; export default defineComponent({ name: "AnnotationList", components: { - RecordingList, + AnnotationEditor, + RecordingAnnotations, }, props: { spectroInfo: { type: Object as PropType, default: () => undefined, }, + selectedAnnotation: { + type: Object as PropType, + default: () => null, + }, + species: { + type: Array as PropType, + required: true, + }, + recordingId: { + type: String, + required: true, + } }, - emits: ['select'], + emits: ['select', 'update:annotation', 'delete:annotation'], setup() { - const { creationType, annotationState, setAnnotationState, annotations, temporalAnnotations, selectedId, selectedType, setSelectedId } = useState(); - const tab = ref('pulse'); + const { creationType, annotationState, setAnnotationState, annotations, temporalAnnotations, selectedId, selectedType, setSelectedId, sideTab } = useState(); + const tab = ref('recording'); const scrollToId = (id: number) => { const el = document.getElementById(`annotation-${id}`); if (el) { @@ -37,13 +51,13 @@ export default defineComponent({ }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const tabSwitch = (event: any) => { - // On tab switches we want to deselect the curret anntation + // On tab switches we want to deselect the curret annotation if (['sequence', 'pulse'].includes(event)) { tab.value = event as 'sequence' | 'pulse'; selectedType.value = event as 'sequence' | 'pulse'; selectedId.value = null; } else { - tab.value = 'recordings'; + tab.value = 'recording'; } }; @@ -58,164 +72,200 @@ export default defineComponent({ setSelectedId, tabSwitch, tab, + sideTab, }; }, }); diff --git a/client/src/components/RecordingAnnotationSummary.vue b/client/src/components/RecordingAnnotationSummary.vue new file mode 100644 index 0000000..eaec45e --- /dev/null +++ b/client/src/components/RecordingAnnotationSummary.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/client/src/components/RecordingAnnotations.vue b/client/src/components/RecordingAnnotations.vue new file mode 100644 index 0000000..72a8b4d --- /dev/null +++ b/client/src/components/RecordingAnnotations.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/client/src/use/useState.ts b/client/src/use/useState.ts index 20ca538..475a9bb 100644 --- a/client/src/use/useState.ts +++ b/client/src/use/useState.ts @@ -21,6 +21,7 @@ const nextShared: Ref = ref(false); const blackBackground = ref(true); const scaledVals: Ref<{x: number, y: number}> = ref({x: 0, y: 0}); const viewCompressedOverlay = ref(false); +const sideTab: Ref<'annotations' | 'recordings'> = ref('annotations'); type AnnotationState = "" | "editing" | "creating" | "disabled"; export default function useState() { @@ -97,5 +98,6 @@ export default function useState() { blackBackground, scaledVals, viewCompressedOverlay, + sideTab, }; } diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index 9275072..50fa104 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -6,12 +6,14 @@ import MapLocation from '../components/MapLocation.vue'; import useState from '../use/useState'; import BatchUploadRecording from '../components/BatchUploadRecording.vue'; import RecordingInfoDisplay from '../components/RecordingInfoDisplay.vue'; +import RecordingAnnotationSummary from '../components/RecordingAnnotationSummary.vue'; export default defineComponent({ components: { UploadRecording, MapLocation, BatchUploadRecording, - RecordingInfoDisplay + RecordingInfoDisplay, + RecordingAnnotationSummary, }, setup() { const itemsPerPage = ref(-1); @@ -26,11 +28,14 @@ export default defineComponent({ title:'Edit', key:'edit', }, - { title:'Name', key:'name', }, + { + title: 'Annotation', + key:'annotation' + }, { title:'Owner', key:'owner_username', @@ -39,10 +44,6 @@ export default defineComponent({ title:'Recorded Date', key:'recorded_date', }, - { - title:'Recorded Time', - key:'recorded_time', - }, { title:'Public', key:'public', @@ -71,6 +72,10 @@ export default defineComponent({ title:'Name', key:'name', }, + { + title: 'Annotation', + key:'annotation' + }, { title:'Owner', key:'owner_username', @@ -79,10 +84,6 @@ export default defineComponent({ title:'Recorded Date', key:'recorded_date', }, - { - title:'Recorded Time', - key:'recorded_time', - }, { title:'Public', key:'public', @@ -251,6 +252,14 @@ export default defineComponent({ + + + + + +