diff --git a/bats_ai/api.py b/bats_ai/api.py index 87a6469b..4a899cc7 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -4,6 +4,7 @@ from oauth2_provider.models import AccessToken from bats_ai.core.views import ( + ConfigurationRouter, GRTSCellsRouter, GuanoMetadataRouter, RecordingAnnotationRouter, @@ -35,3 +36,4 @@ def global_auth(request): api.add_router('/grts/', GRTSCellsRouter) api.add_router('/guano/', GuanoMetadataRouter) api.add_router('/recording-annotation/', RecordingAnnotationRouter) +api.add_router('/configuration/', ConfigurationRouter) diff --git a/bats_ai/core/migrations/0013_configuration.py b/bats_ai/core/migrations/0013_configuration.py new file mode 100644 index 00000000..75c20eff --- /dev/null +++ b/bats_ai/core/migrations/0013_configuration.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.13 on 2025-01-03 15:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0012_recordingannotation_additional_data'), + ] + + operations = [ + migrations.CreateModel( + name='Configuration', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ('display_pulse_annotations', models.BooleanField(default=True)), + ('display_sequence_annotations', models.BooleanField(default=True)), + ], + ), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index f53dc0b0..16944b1d 100644 --- a/bats_ai/core/models/__init__.py +++ b/bats_ai/core/models/__init__.py @@ -1,5 +1,6 @@ from .annotations import Annotations from .compressed_spectrogram import CompressedSpectrogram +from .configuration import Configuration from .grts_cells import GRTSCells from .image import Image from .recording import Recording, colormap @@ -21,4 +22,5 @@ 'colormap', 'CompressedSpectrogram', 'RecordingAnnotation', + 'Configuration', ] diff --git a/bats_ai/core/models/configuration.py b/bats_ai/core/models/configuration.py new file mode 100644 index 00000000..fad37fab --- /dev/null +++ b/bats_ai/core/models/configuration.py @@ -0,0 +1,25 @@ +from django.db import models +from django.db.models.signals import post_migrate +from django.dispatch import receiver + + +# Define the Configuration model +class Configuration(models.Model): + display_pulse_annotations = models.BooleanField(default=True) + display_sequence_annotations = models.BooleanField(default=True) + + def save(self, *args, **kwargs): + # Ensure only one instance of Configuration exists + if not Configuration.objects.exists() and not self.pk: + super().save(*args, **kwargs) + elif self.pk: + super().save(*args, **kwargs) + else: + raise ValueError('Only one instance of Configuration is allowed.') + + +# Automatically create a Configuration instance after migrations +@receiver(post_migrate) +def create_default_configuration(sender, **kwargs): + if not Configuration.objects.exists(): + Configuration.objects.create() diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index 348849ce..37d82d72 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -1,4 +1,5 @@ from .annotations import router as AnnotationRouter +from .configuration import router as ConfigurationRouter from .grts_cells import router as GRTSCellsRouter from .guanometadata import router as GuanoMetadataRouter from .recording import router as RecordingRouter @@ -14,4 +15,5 @@ 'GRTSCellsRouter', 'GuanoMetadataRouter', 'RecordingAnnotationRouter', + 'ConfigurationRouter', ] diff --git a/bats_ai/core/views/configuration.py b/bats_ai/core/views/configuration.py new file mode 100644 index 00000000..dd85597c --- /dev/null +++ b/bats_ai/core/views/configuration.py @@ -0,0 +1,53 @@ +import logging + +from django.http import JsonResponse +from ninja import Schema +from ninja.pagination import RouterPaginated + +from bats_ai.core.models import Configuration + +logger = logging.getLogger(__name__) + + +router = RouterPaginated() + + +# Define schema for the Configuration data +class ConfigurationSchema(Schema): + display_pulse_annotations: bool + display_sequence_annotations: bool + is_admin: bool | None = None + + +# Endpoint to retrieve the configuration status +@router.get('/', response=ConfigurationSchema) +def get_configuration(request): + config = Configuration.objects.first() + if not config: + return JsonResponse({'error': 'No configuration found'}, status=404) + return ConfigurationSchema( + display_pulse_annotations=config.display_pulse_annotations, + display_sequence_annotations=config.display_sequence_annotations, + is_admin=request.user.is_authenticated and request.user.is_superuser, + ) + + +# Endpoint to update the configuration (admin only) +@router.patch('/') +def update_configuration(request, payload: ConfigurationSchema): + if not request.user.is_authenticated or not request.user.is_superuser: + return JsonResponse({'error': 'Permission denied'}, status=403) + config = Configuration.objects.first() + if not config: + return JsonResponse({'error': 'No configuration found'}, status=404) + for attr, value in payload.dict().items(): + setattr(config, attr, value) + config.save() + return ConfigurationSchema.from_orm(config) + + +@router.get('/is_admin/') +def check_is_admin(request): + if request.user.is_authenticated: + return {'is_admin': request.user.is_superuser} + return {'is_admin': False} diff --git a/bats_ai/core/views/temporal_annotations.py b/bats_ai/core/views/temporal_annotations.py index 0495acbb..3a4294d4 100644 --- a/bats_ai/core/views/temporal_annotations.py +++ b/bats_ai/core/views/temporal_annotations.py @@ -12,7 +12,7 @@ class TemporalAnnotationSchema(Schema): id: int start_time: int end_time: int - type: str + type: str | None comments: str species: list[SpeciesSchema] | None owner_email: str = None diff --git a/client/src/App.vue b/client/src/App.vue index df706fa8..4a594d0b 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -12,21 +12,22 @@ export default defineComponent({ const oauthClient = inject("oauthClient"); const router = useRouter(); const route = useRoute(); - const { nextShared, sharedList, sideTab, } = useState(); + const { nextShared, sharedList, sideTab, loadConfiguration, configuration } = useState(); const getShared = async () => { sharedList.value = (await getRecordings(true)).data; }; - if (sharedList.value.length === 0) { - getShared(); - } if (oauthClient === undefined) { throw new Error('Must provide "oauthClient" into component.'); } const loginText = ref("Login"); - const checkLogin = () => { + const checkLogin = async () => { if (oauthClient.isLoggedIn) { loginText.value = "Logout"; + loadConfiguration(); + if (sharedList.value.length === 0) { + getShared(); + } } else { loginText.value = "Login"; } @@ -57,7 +58,18 @@ export default defineComponent({ } }); - return { oauthClient, containsSpectro, loginText, logInOrOut, activeTab, nextShared, sideTab }; + const isAdmin = computed(() => configuration.value.is_admin); + + return { + oauthClient, + containsSpectro, + loginText, + logInOrOut, + activeTab, + nextShared, + sideTab, + isAdmin, + }; }, }); @@ -88,6 +100,13 @@ export default defineComponent({ > Spectrogram + + Admin + (`/grts/grid_cell_id`, {params: {latitude, longitude}}); } +export interface ConfigurationSettings { +display_pulse_annotations: boolean; +display_sequence_annotations: boolean; +is_admin: boolean; +} + +export type Configuration = ConfigurationSettings & { is_admin : boolean }; +async function getConfiguration() { + return axiosInstance.get('/configuration/'); +} + +async function patchConfiguration(config: ConfigurationSettings) { + return axiosInstance.patch('/configuration/', {...config }); +} + interface GuanoMetadata { nabat_grid_cell_grts_id?: string nabat_latitude?: number @@ -409,4 +424,6 @@ export { putFileAnnotation, patchFileAnnotation, deleteFileAnnotation, + getConfiguration, + patchConfiguration, }; \ No newline at end of file diff --git a/client/src/components/AnnotationList.vue b/client/src/components/AnnotationList.vue index d06034be..51d4d7a1 100644 --- a/client/src/components/AnnotationList.vue +++ b/client/src/components/AnnotationList.vue @@ -1,5 +1,5 @@ + + + + \ No newline at end of file