diff --git a/alyx/experiments/serializers.py b/alyx/experiments/serializers.py index 92ad06a8..1697086e 100644 --- a/alyx/experiments/serializers.py +++ b/alyx/experiments/serializers.py @@ -1,3 +1,4 @@ +from django.db.models import Prefetch from rest_framework import serializers from alyx.base import BaseSerializerEnumField from actions.models import Session @@ -5,24 +6,22 @@ Channel, BrainRegion, ChronicInsertion, FOV, FOVLocation, ImagingType, ImagingStack) from data.models import DatasetType, Dataset, DataRepository, FileRecord -from subjects.models import Subject +from subjects.models import Subject, Project from misc.models import Lab class SessionListSerializer(serializers.ModelSerializer): - - @staticmethod - def setup_eager_loading(queryset): - """ Perform necessary eager loading of data to avoid horrible performance.""" - queryset = queryset.select_related('subject', 'lab') - return queryset.order_by('-start_time') - + """Session model serializer within ProbeInsertion and ChronicProbeInsertion serializers.""" subject = serializers.SlugRelatedField(read_only=True, slug_field='nickname') lab = serializers.SlugRelatedField(read_only=True, slug_field='name') + projects = serializers.SlugRelatedField(read_only=False, + slug_field='name', + queryset=Project.objects.all(), + many=True) class Meta: model = Session - fields = ('subject', 'start_time', 'number', 'lab', 'id', 'task_protocol') + fields = ('subject', 'start_time', 'number', 'lab', 'id', 'projects', 'task_protocol') class TrajectoryEstimateSerializer(serializers.ModelSerializer): @@ -104,9 +103,13 @@ class ChronicProbeInsertionListSerializer(serializers.ModelSerializer): @staticmethod def setup_eager_loading(queryset): - """ Perform necessary eager loading of data to avoid horrible performance.""" - queryset = queryset.select_related('session__subject__nickname') - return queryset + """Perform necessary eager loading of data to avoid horrible performance. + + SessionListSerializer uses these related tables. + """ + queryset = queryset.select_related('model', 'session', 'session__subject', 'session__lab') + queryset = queryset.prefetch_related('session__projects') + return queryset.order_by('-session__start_time') model = serializers.SlugRelatedField(read_only=True, slug_field='name') session_info = SessionListSerializer(read_only=True, source='session') @@ -121,8 +124,8 @@ class ProbeInsertionListSerializer(serializers.ModelSerializer): @staticmethod def setup_eager_loading(queryset): """ Perform necessary eager loading of data to avoid horrible performance.""" - queryset = queryset.select_related('model', 'session') - queryset = queryset.prefetch_related('session__subject', 'session__lab', 'datasets') + queryset = queryset.select_related('model', 'session', 'session__subject', 'session__lab') + queryset = queryset.prefetch_related('session__projects', 'datasets') return queryset.order_by('-session__start_time') session = serializers.SlugRelatedField( @@ -152,6 +155,14 @@ class Meta: class ProbeInsertionDetailSerializer(serializers.ModelSerializer): + + @staticmethod + def setup_eager_loading(queryset): + """ Perform necessary eager loading of data to avoid horrible performance.""" + queryset = queryset.select_related('model', 'session', 'session__subject', 'session__lab') + queryset = queryset.prefetch_related('session__projects') + return queryset.order_by('-session__start_time') + session = serializers.SlugRelatedField( read_only=False, required=False, slug_field='id', queryset=Session.objects.all(), @@ -165,8 +176,7 @@ class ProbeInsertionDetailSerializer(serializers.ModelSerializer): datasets = serializers.SerializerMethodField() def get_datasets(self, obj): - qs = obj.session.data_dataset_session_related.all().filter(collection__icontains=obj.name) - + qs = obj.session.data_dataset_session_related.filter(collection__icontains=obj.name) request = self.context.get('request', None) dsets = ProbeInsertionDatasetsSerializer(qs, many=True, context={'request': request}) return dsets.data @@ -193,19 +203,17 @@ def setup_eager_loading(queryset): read_only=False, required=False, slug_field='probe_model', queryset=ProbeModel.objects.all(), ) - lab = serializers.SlugRelatedField( read_only=False, required=False, slug_field='name', queryset=Lab.objects.all(), ) - probe_insertion = serializers.SerializerMethodField() def get_probe_insertion(self, obj): - qs = obj.probe_insertion.all() + qs = ChronicProbeInsertionListSerializer.setup_eager_loading(obj.probe_insertion.all()) request = self.context.get('request', None) - dsets = ChronicProbeInsertionListSerializer(qs, many=True, context={'request': request}) - return dsets.data + ins = ChronicProbeInsertionListSerializer(qs, many=True, context={'request': request}) + return ins.data class Meta: model = ChronicInsertion @@ -232,7 +240,7 @@ class ChronicInsertionDetailSerializer(serializers.ModelSerializer): probe_insertion = serializers.SerializerMethodField() def get_probe_insertion(self, obj): - qs = obj.probe_insertion.all() + qs = ChronicProbeInsertionListSerializer.setup_eager_loading(obj.probe_insertion.all()) request = self.context.get('request', None) dsets = ChronicProbeInsertionListSerializer(qs, many=True, context={'request': request}) return dsets.data @@ -297,9 +305,12 @@ class FOVSerializer(serializers.ModelSerializer): @staticmethod def setup_eager_loading(queryset): """Perform necessary eager loading of data to avoid horrible performance.""" - queryset = queryset.select_related('model', 'session') + queryset = queryset.select_related('imaging_type') + # Apply eager loading to the nested location field + location_qs = FOVLocationListSerializer.setup_eager_loading(FOVLocation.objects.all()) queryset = queryset.prefetch_related( - 'session__subject', 'session__lab', 'location', 'datasets') + 'datasets', Prefetch('location', queryset=location_qs) + ) return queryset.order_by('-session__start_time') class Meta: @@ -313,9 +324,11 @@ class ImagingStackDetailSerializer(serializers.ModelSerializer): @staticmethod def setup_eager_loading(queryset): - """Perform necessary eager loading of data to avoid horrible performance.""" - queryset = queryset.prefetch_related('slices') - return queryset.order_by('slices__z__0') + """Perform necessary eager loading of nested slices.""" + slice_qs = FOVSerializer.setup_eager_loading(FOV.objects.filter(stack__isnull=False)) + queryset = queryset.prefetch_related(Prefetch('slices', queryset=slice_qs)) + # TODO order by z values of FOVLocations where default_provenance is True + return queryset.order_by('slices__name') class Meta: model = ImagingStack diff --git a/alyx/experiments/tests_rest.py b/alyx/experiments/tests_rest.py index f7e2dc9f..d9a57247 100644 --- a/alyx/experiments/tests_rest.py +++ b/alyx/experiments/tests_rest.py @@ -7,7 +7,7 @@ from alyx.base import BaseTests from actions.models import Session, ProcedureType from misc.models import Lab -from subjects.models import Subject +from subjects.models import Subject, Project from experiments.models import ProbeInsertion, ImagingType from data.models import Dataset, DatasetType, Tag @@ -22,6 +22,7 @@ def setUp(self): self.session = Session.objects.first() # need to add ephys procedure self.session.task_protocol = 'ephys' + self.session.projects.add(Project.objects.get_or_create(name='brain_wide')[0]) self.session.save() self.dict_insertion = {'session': str(self.session.id), 'name': 'probe_00', @@ -76,6 +77,9 @@ def test_create_list_delete_probe_insertion(self): # test the list endpoint response = self.client.get(url) d = self.ar(response, 200) + self.assertIn('session_info', d[0]) + # Ensure the session_info includes the projects as a list of names + self.assertCountEqual(d[0]['session_info'].get('projects', []), ['brain_wide']) # test the session filter urlf = url + '?&session=' + str(self.session.id) + '&name=probe_00' @@ -112,7 +116,7 @@ def test_probe_insertion_rest(self): self.assertTrue(len(probe_ins) == 0) # test the project filter - urlf = (reverse('probeinsertion-list') + '?&project=brain_wide') + urlf = (reverse('probeinsertion-list') + '?&project=foobar') probe_ins = self.ar(self.client.get(urlf)) self.assertTrue(len(probe_ins) == 0) diff --git a/alyx/experiments/urls.py b/alyx/experiments/urls.py index 1c2ca894..4579d42a 100644 --- a/alyx/experiments/urls.py +++ b/alyx/experiments/urls.py @@ -17,5 +17,6 @@ path('fields-of-view', ev.FOVList.as_view(), name="fieldsofview-list"), path('fields-of-view/', ev.FOVDetail.as_view(), name="fieldsofview-detail"), path('fov-location', ev.FOVLocationList.as_view(), name="fovlocation-list"), + path('fov-location/', ev.FOVLocationDetail.as_view(), name="fovlocation-detail"), path('imaging-stack', ev.ImagingStackList.as_view(), name="imagingstack-list"), path('imaging-stack/', ev.ImagingStackDetail.as_view(), name="imagingstack-detail")] diff --git a/alyx/experiments/views.py b/alyx/experiments/views.py index 233dd8db..616a2d70 100644 --- a/alyx/experiments/views.py +++ b/alyx/experiments/views.py @@ -168,6 +168,7 @@ class ProbeInsertionList(generics.ListCreateAPIView): class ProbeInsertionDetail(generics.RetrieveUpdateDestroyAPIView): queryset = ProbeInsertion.objects.all() + queryset = ProbeInsertionDetailSerializer.setup_eager_loading(queryset) serializer_class = ProbeInsertionDetailSerializer permission_classes = rest_permission_classes() @@ -445,6 +446,7 @@ class FOVList(generics.ListCreateAPIView): [===> FOV model reference](/admin/doc/models/experiments.fov) """ queryset = FOV.objects.all() + queryset = FOVSerializer.setup_eager_loading(queryset) serializer_class = FOVSerializer permission_classes = rest_permission_classes() filterset_class = FOVFilter @@ -485,6 +487,7 @@ class FOVLocationList(generics.ListCreateAPIView): [===> FOVLocation model reference](/admin/doc/models/experiments.fovlocation) """ queryset = FOVLocation.objects.all() + queryset = FOVLocationListSerializer.setup_eager_loading(queryset) permission_classes = rest_permission_classes() filterset_class = FOVLocationFilter @@ -499,6 +502,7 @@ def get_serializer_class(self): class FOVLocationDetail(generics.RetrieveUpdateDestroyAPIView): queryset = FOVLocation.objects.all() + queryset = FOVLocationDetailSerializer.setup_eager_loading(queryset) serializer_class = FOVLocationDetailSerializer permission_classes = rest_permission_classes() @@ -534,7 +538,7 @@ class ImagingStackList(generics.ListCreateAPIView): [===> ImagingStack model reference](/admin/doc/models/experiments.imagingstack) """ queryset = ImagingStack.objects.all() - # serializer_class = ImagingStackListSerializer + queryset = ImagingStackDetailSerializer.setup_eager_loading(queryset) permission_classes = rest_permission_classes() filterset_class = ImagingStackFilter @@ -550,5 +554,6 @@ class ImagingStackDetail(generics.RetrieveAPIView): [===> ImagingStack model reference](/admin/doc/models/experiments.imagingstack) """ queryset = ImagingStack.objects.all() + queryset = ImagingStackDetailSerializer.setup_eager_loading(queryset) serializer_class = ImagingStackDetailSerializer permission_classes = rest_permission_classes()