diff --git a/alyx/alyx/__init__.py b/alyx/alyx/__init__.py index d08f5d9b..e7b3befd 100644 --- a/alyx/alyx/__init__.py +++ b/alyx/alyx/__init__.py @@ -1 +1 @@ -VERSION = __version__ = '1.16.2' +VERSION = __version__ = '1.17.0' diff --git a/alyx/data/fixtures/data.datasettype.json b/alyx/data/fixtures/data.datasettype.json index 9c02f9b8..910d5550 100644 --- a/alyx/data/fixtures/data.datasettype.json +++ b/alyx/data/fixtures/data.datasettype.json @@ -2209,5 +2209,16 @@ "description": "Unique identifier assigned to each ROI when ALF files created after ROI detection.", "filename_pattern": "" } + }, + { + "model": "data.datasettype", + "pk": "e0a71fbc-f8ff-42a1-bfa7-d7764ab4104d", + "fields": { + "json": null, + "name": "photometryROI.locations", + "created_by": null, + "description": "Look up table from photometry ROI, to fiber name registered in the database and Allen brain location", + "filename_pattern": "*photometryROI.locations*" + } } ] diff --git a/alyx/misc/serializers.py b/alyx/misc/serializers.py index e74f8c7e..464c2983 100644 --- a/alyx/misc/serializers.py +++ b/alyx/misc/serializers.py @@ -50,6 +50,8 @@ class Meta: class UserSerializer(serializers.ModelSerializer): subjects_responsible = serializers.SlugRelatedField( many=True, queryset=Subject.objects.all(), slug_field='nickname') + allowed_users = serializers.SlugRelatedField( + many=True, queryset=LabMember.objects.all(), slug_field='username') @staticmethod def setup_eager_loading(queryset): @@ -58,7 +60,7 @@ def setup_eager_loading(queryset): class Meta: model = get_user_model() - fields = ('id', 'username', 'email', 'subjects_responsible', 'lab') + fields = ('id', 'username', 'email', 'subjects_responsible', 'lab', 'allowed_users') class LabSerializer(serializers.HyperlinkedModelSerializer): diff --git a/alyx/misc/tests_rest.py b/alyx/misc/tests_rest.py index 9a3c26c1..37d2baa7 100644 --- a/alyx/misc/tests_rest.py +++ b/alyx/misc/tests_rest.py @@ -20,6 +20,7 @@ def setUp(self): self.lab = Lab.objects.create(name='basement') self.public_user = get_user_model().objects.create( username='troublemaker', password='azerty', is_public_user=True) + self.public_user.allowed_users.set([self.superuser]) def test_create_lab_membership(self): # first test creation of lab through rest endpoint @@ -55,6 +56,12 @@ def test_public_user(self): def test_user_rest(self): response = self.client.get(reverse('user-list') + '/test') self.ar(response, 200) + response = self.client.get(reverse('user-list') + '/troublemaker') + self.ar(response, 200) + self.assertEqual( + response.data.get('allowed_users'), + list(self.public_user.allowed_users.all().values_list('username', flat=True)) + ) def test_note_rest(self): user = self.ar(self.client.get(reverse('user-list')), 200) @@ -71,7 +78,7 @@ def test_note_rest(self): class TestCacheView(BaseTests): def setUp(self): - # This doesn't need super user privilages but I didn't know how to create a normal user + # This doesn't need super user privileges but I didn't know how to create a normal user self.superuser = get_user_model().objects.create_user('test', 'test', 'test') self.client.login(username='test', password='test') self.tag = Tag.objects.create(name='2022_Q1_paper') diff --git a/alyx/misc/urls.py b/alyx/misc/urls.py index 05db6211..a73f12ec 100644 --- a/alyx/misc/urls.py +++ b/alyx/misc/urls.py @@ -4,17 +4,14 @@ from django.conf.urls import include -user_list = mv.UserViewSet.as_view({'get': 'list'}) -user_detail = mv.UserViewSet.as_view({'get': 'retrieve'}) - urlpatterns = [ path('', RedirectView.as_view(url='/admin')), # redirect the page to admin interface path('labs', mv.LabList.as_view(), name="lab-list"), path('labs/', mv.LabDetail.as_view(), name="lab-detail"), path('notes', mv.NoteList.as_view(), name="note-list"), path('notes/', mv.NoteDetail.as_view(), name="note-detail"), - path('users/', user_detail, name='user-detail'), - path('users', user_list, name='user-list'), + path('users', mv.UserList.as_view(), name="user-list"), + path('users/', mv.UserDetail.as_view(), name="user-detail"), re_path('^uploaded/(?P.*)', mv.UploadedView.as_view(), name='uploaded'), path('cache.zip', mv.CacheDownloadView.as_view(), name='cache-download'), re_path(r'^cache/info(?:/(?P\w+))?/$', mv.CacheVersionView.as_view(), name='cache-info'), diff --git a/alyx/misc/views.py b/alyx/misc/views.py index 15ade025..4423a778 100644 --- a/alyx/misc/views.py +++ b/alyx/misc/views.py @@ -10,7 +10,7 @@ HttpResponse, FileResponse, JsonResponse, HttpResponseRedirect, HttpResponseNotFound ) -from rest_framework import viewsets, views +from rest_framework import views from rest_framework.response import Response from rest_framework.decorators import api_view from rest_framework.reverse import reverse @@ -69,15 +69,35 @@ def api_root(request, format=None): }) -class UserViewSet(viewsets.ReadOnlyModelViewSet): +class UserFilter(BaseFilterSet): + class Meta: + model = get_user_model() + exclude = ['json'] + + +class UserList(generics.ListCreateAPIView): """ - Lists all users with the subjects which they are responsible for. + get: **FILTERS** + - 'id' + - 'username' + - 'email' + - 'subjects_responsible' + - 'lab' + - 'allowed_users' + [===> user model reference](/admin/doc/models/misc.labmember) """ - queryset = get_user_model().objects.all() - queryset = UserSerializer.setup_eager_loading(queryset) + queryset = UserSerializer.setup_eager_loading(get_user_model().objects.all()) serializer_class = UserSerializer + permission_classes = rest_permission_classes() + filter_class = UserFilter lookup_field = 'username' + + +class UserDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = UserSerializer.setup_eager_loading(get_user_model().objects.all()) + serializer_class = UserSerializer permission_classes = rest_permission_classes() + lookup_field = 'username' class LabFilter(BaseFilterSet): diff --git a/requirements_frozen.txt b/requirements_frozen.txt index bd2ad972..c7154991 100644 --- a/requirements_frozen.txt +++ b/requirements_frozen.txt @@ -1,8 +1,8 @@ asgiref==3.7.2 backports.zoneinfo==0.2.1 -boto3==1.28.78 -botocore==1.31.78 -certifi==2023.7.22 +boto3==1.33.4 +botocore==1.33.4 +certifi==2023.11.17 cffi==1.16.0 charset-normalizer==3.3.2 click==8.1.7 @@ -12,7 +12,7 @@ coreapi==2.3.3 coreschema==0.0.4 coverage==6.5.0 coveralls==3.3.1 -cryptography==41.0.5 +cryptography==41.0.7 cycler==0.12.1 Django==4.2.7 django-admin-list-filter-dropdown==1.0.3 @@ -24,22 +24,22 @@ django-ipware==5.0.2 django-js-asset==2.1.0 django-mptt==0.14.0 django-polymorphic==3.1.0 -django-reversion==5.0.6 +django-reversion==5.0.8 django-storages==1.14.2 -django-structlog==6.0.0 +django-structlog==6.0.1 django-test-without-migrations==0.6 djangorestframework==3.14.0 docopt==0.6.2 docutils==0.20.1 drfdocs==0.0.11 flake8==6.1.0 -fonttools==4.44.0 -globus-cli==3.18.0 -globus-sdk==3.28.0 +fonttools==4.45.1 +globus-cli==3.19.0 +globus-sdk==3.30.0 iblutil==1.7.1 -idna==3.4 +idna==3.6 importlib-metadata==6.8.0 -importlib-resources==6.1.0 +importlib-resources==6.1.1 itypes==1.2.0 Jinja2==3.1.2 jmespath==1.0.1 @@ -48,7 +48,7 @@ llvmlite==0.41.1 lxml==4.9.3 Markdown==3.5.1 MarkupSafe==2.1.3 -matplotlib==3.7.3 +matplotlib==3.7.4 mccabe==0.7.0 numba==0.58.1 numpy==1.24.4 @@ -57,7 +57,7 @@ packaging==23.2 pandas==2.0.3 Pillow==10.1.0 psycopg2-binary==2.9.9 -pyarrow==14.0.0 +pyarrow==14.0.1 pycodestyle==2.11.1 pycparser==2.21 pyflakes==3.1.0 @@ -68,7 +68,7 @@ python-magic==0.4.27 pytz==2023.3.post1 PyYAML==6.0.1 requests==2.31.0 -s3transfer==0.7.0 +s3transfer==0.8.2 six==1.16.0 sqlparse==0.4.4 structlog==23.2.0 diff --git a/scripts/sync_ucl/prune_cortexlab.py b/scripts/sync_ucl/prune_cortexlab.py index 441131d8..e3dbd909 100755 --- a/scripts/sync_ucl/prune_cortexlab.py +++ b/scripts/sync_ucl/prune_cortexlab.py @@ -83,7 +83,8 @@ # only imports users that are relevant to IBL users_to_import = ['cyrille', 'Gaelle', 'kenneth', 'lauren', 'matteo', 'miles', 'nick', 'olivier', - 'Karolina_Socha', 'Hamish', 'laura', 'niccolo', 'SamuelP', 'miriam.jansen', 'carolina.quadrado'] + 'Karolina_Socha', 'Hamish', 'laura', 'niccolo', 'SamuelP', 'miriam.jansen', + 'carolina.quadrado'] users_to_leave = LabMember.objects.using('cortexlab').exclude(username__in=users_to_import) users_to_keep = Dataset.objects.using('cortexlab').values_list('created_by', flat=True).distinct() users_to_leave = users_to_leave.exclude(pk__in=users_to_keep)