Skip to content

Commit

Permalink
Merge pull request #896 from cortex-lab/dev
Browse files Browse the repository at this point in the history
3.1.3
  • Loading branch information
k1o0 authored Jan 8, 2025
2 parents 378e2f1 + 50755ac commit f256572
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 53 deletions.
8 changes: 8 additions & 0 deletions alyx/actions/fixtures/actions.watertype.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,13 @@
"json": null,
"name": "Hydrogel"
}
},
{
"model": "actions.watertype",
"pk": "dba3e45a-fb95-4a6d-9140-2b704c5b300e",
"fields": {
"json": null,
"name": "Water 1% Citric Acid"
}
}
]
7 changes: 5 additions & 2 deletions alyx/actions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ class BaseActionSerializer(serializers.HyperlinkedModelSerializer):
slug_field='username',
queryset=get_user_model().objects.all(),
required=False,
default=serializers.CurrentUserDefault(),
)

location = serializers.SlugRelatedField(
Expand All @@ -57,7 +56,6 @@ class BaseActionSerializer(serializers.HyperlinkedModelSerializer):
queryset=LabLocation.objects.all(),
allow_null=True,
required=False,

)

procedures = serializers.SlugRelatedField(
Expand All @@ -76,6 +74,11 @@ class BaseActionSerializer(serializers.HyperlinkedModelSerializer):
many=False,
required=False,)

def create(self, validated_data):
if not validated_data.get('users'):
validated_data['users'] = [self.context['request'].user]
return super().create(validated_data)


class LabLocationSerializer(serializers.ModelSerializer):

Expand Down
18 changes: 9 additions & 9 deletions alyx/actions/tests_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def test_sessions_projects(self):
def test_sessions(self):
a_dict4json = {'String': 'this is not a JSON', 'Integer': 4, 'List': ['titi', 4]}
ses_dict = {'subject': self.subject.nickname,
'users': [self.superuser.username],
'users': [self.superuser2.username],
'projects': [self.projectX.name, self.projectY.name],
'narrative': 'auto-generated-session, test',
'start_time': '2018-07-09T12:34:56',
Expand Down Expand Up @@ -223,24 +223,24 @@ def test_sessions(self):
# create another session for further testing
ses_dict['start_time'] = '2018-07-11T12:34:56'
ses_dict['end_time'] = '2018-07-11T12:34:57'
ses_dict['users'] = [self.superuser.username, self.superuser2.username]
# should use default user when not provided
del ses_dict['users']
ses_dict['lab'] = self.lab02.name
ses_dict['n_correct_trials'] = 37
r = self.post(reverse('session-list'), data=ses_dict)
s2 = self.ar(r, code=201)
self.assertEqual(['test'], s2['users'])
s2.pop('json')
# Test the date range filter
r = self.client.get(reverse('session-list') + '?date_range=2018-07-09,2018-07-09')
rdata = self.ar(r)
self.assertEqual(rdata[0], s1)
# Test the user filter, this should return 2 sessions
# Test the user filter, this should return 1 session
d = self.ar(self.client.get(reverse('session-list') + '?users=test'))
self.assertEqual(len(d), 2)
# This should return only one session
d = self.ar(self.client.get(reverse('session-list') + '?users=test2'))
self.assertEqual(len(d), 1)
for k in d[0]:
self.assertEqual(d[0][k], s2[k])
# This should return 0 sessions
d = self.ar(self.client.get(reverse('session-list') + '?users=foo'))
self.assertEqual(len(d), 0)
# This should return only one session
d = self.ar(self.client.get(reverse('session-list') + '?lab=awesomelab'))
self.assertEqual(len(d), 1)
Expand All @@ -254,7 +254,7 @@ def test_sessions(self):
self.assertEqual(d[0]['url'], s2['url'])
self.assertEqual(1, len(d))
# test the Session serializer water admin related field
ses = Session.objects.get(subject=self.subject, users=self.superuser,
ses = Session.objects.get(subject=self.subject, users=self.superuser2,
lab__name='superlab', start_time__date='2018-07-09')
WaterAdministration.objects.create(subject=self.subject, session=ses, water_administered=1)
d = self.ar(self.client.get(reverse('session-list') + '?date_range=2018-07-09,2018-07-09'))
Expand Down
2 changes: 1 addition & 1 deletion alyx/alyx/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = __version__ = '3.1.2'
VERSION = __version__ = '3.1.3'
1 change: 1 addition & 0 deletions alyx/alyx/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ def has_change_permission(self, request, obj=None):
# [CR 2024-03-12]
# HACK: following a request by Charu R from cortexlab, we authorize all users in the
# special Husbandry group to edit litters.
# FIXME This should be moved to the individual model admin has_change_permission methods
husbandry = 'husbandry' in ', '.join(_.name.lower() for _ in request.user.groups.all())
if husbandry:
if obj.__class__.__name__ in ('Litter', 'Subject', 'BreedingPair'):
Expand Down
21 changes: 16 additions & 5 deletions alyx/data/fixtures/data.datasettype.json
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@
"name": "_iblrig_taskData.raw",
"created_by": null,
"description": "Data file saved by PyBpod in json serializable lines (file itself is not a json object, each line is a json object corresponding to a trial) - Training task w/ automated contrast",
"filename_pattern": "_iblrig_taskData.raw.*"
"filename_pattern": "_*_taskData.raw.*"
}
},
{
Expand All @@ -589,7 +589,7 @@
"json": null,
"name": "passiveWhiteNoise.times",
"created_by": null,
"description": "Times of white noise bursts, equivilent to the negative feedback sound during the choice world task",
"description": "Times of white noise bursts, equivalent to the negative feedback sound during the choice world task",
"filename_pattern": "*passiveWhiteNoise.times.*"
}
},
Expand Down Expand Up @@ -1810,7 +1810,7 @@
"json": null,
"name": "subjectTrials.table",
"created_by": null,
"description": "All trials data for a given subject, contains the same columns as trials.table, plus \"session\", \"session_start_time\" and \"session_number\"",
"description": "All trials data for a given subject, contains the same columns as _ibl_trials.table, plus additional trials data: \"goCueTrigger_times\", \"stimOnTrigger_times\", \"stimFreezeTrigger_times\", \"stimFreeze_times\", \"stimOffTrigger_times\", \"stimOff_times\", \"phase\", \"position\", \"quiescence\". The following are session meta data columns: \"session\", \"session_start_time\" and \"task_protocol\", \"protocol_number\"",
"filename_pattern": ""
}
},
Expand Down Expand Up @@ -2338,8 +2338,19 @@
"json": null,
"name": "fpData.digitalInputs",
"created_by": null,
"description": "",
"filename_pattern": "*digitalInputs.raw*"
"description": "Parquet or CSV file output by Bonsai containing the synchronisation TTLs.",
"filename_pattern": "*_fpData.digitalInputs.*"
}
},
{
"model": "data.datasettype",
"pk": "a94ba7c4-9260-4270-98c0-96030248a7b6",
"fields": {
"json": null,
"name": "_sp_video.times",
"created_by": null,
"description": "An array of passive video frame times, where each column represents a repeat of the video.",
"filename_pattern": "_sp_video.times.*"
}
}
]
67 changes: 40 additions & 27 deletions alyx/experiments/serializers.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
from django.db.models import Prefetch
from rest_framework import serializers
from alyx.base import BaseSerializerEnumField
from actions.models import Session
from experiments.models import (ProbeInsertion, TrajectoryEstimate, ProbeModel, CoordinateSystem,
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):
Expand Down Expand Up @@ -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')
Expand All @@ -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(
Expand Down Expand Up @@ -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(),
Expand 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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions alyx/experiments/tests_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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',
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions alyx/experiments/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
path('fields-of-view', ev.FOVList.as_view(), name="fieldsofview-list"),
path('fields-of-view/<uuid:pk>', ev.FOVDetail.as_view(), name="fieldsofview-detail"),
path('fov-location', ev.FOVLocationList.as_view(), name="fovlocation-list"),
path('fov-location/<uuid:pk>', ev.FOVLocationDetail.as_view(), name="fovlocation-detail"),
path('imaging-stack', ev.ImagingStackList.as_view(), name="imagingstack-list"),
path('imaging-stack/<uuid:pk>', ev.ImagingStackDetail.as_view(), name="imagingstack-detail")]
7 changes: 6 additions & 1 deletion alyx/experiments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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()

Expand Down Expand Up @@ -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

Expand All @@ -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()
Loading

0 comments on commit f256572

Please sign in to comment.