From 0767489d5fca48e7fe6d4153a5527cc2947abead Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 6 Sep 2024 10:15:02 -0400 Subject: [PATCH 1/2] Add automated tests for the new moving/renaming functionality for filebrowser's folders, files and link files --- .env | 2 +- chris_backend/config/settings/local.py | 2 +- chris_backend/filebrowser/serializers.py | 8 +- chris_backend/filebrowser/tests/test_views.py | 201 ++++++++++++++---- chris_backend/userfiles/serializers.py | 3 +- .../userfiles/tests/test_serializers.py | 31 +++ 6 files changed, 197 insertions(+), 50 deletions(-) diff --git a/.env b/.env index 2d5c5f1f..ad51ae8d 100755 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ # Variables declared here are available to -# docker-compose on execution +# docker compose on execution CHRISREPO=fnndsc STOREREPO=fnndsc PFCONREPO=fnndsc diff --git a/chris_backend/config/settings/local.py b/chris_backend/config/settings/local.py index 296cd494..29ce2c00 100755 --- a/chris_backend/config/settings/local.py +++ b/chris_backend/config/settings/local.py @@ -101,7 +101,7 @@ MEDIA_ROOT = None if STORAGE_ENV in ('fslink', 'filesystem'): STORAGES['default'] = {'BACKEND': 'django.core.files.storage.FileSystemStorage'} - MEDIA_ROOT = '/data' # local filesystem storage settings + MEDIA_ROOT = '/var/chris' # local filesystem storage settings try: verify_storage_connection( diff --git a/chris_backend/filebrowser/serializers.py b/chris_backend/filebrowser/serializers.py index d1abc203..fd64f45c 100755 --- a/chris_backend/filebrowser/serializers.py +++ b/chris_backend/filebrowser/serializers.py @@ -3,12 +3,10 @@ from django.contrib.auth.models import User, Group from django.db.utils import IntegrityError -from django.conf import settings from rest_framework import serializers from rest_framework.reverse import reverse from collectionjson.fields import ItemLinkField -from core.storage import connect_storage from core.utils import get_file_resource_link from core.models import (ChrisFolder, ChrisFile, ChrisLinkFile, FolderGroupPermission, FolderUserPermission, FileGroupPermission, FileUserPermission, @@ -17,6 +15,7 @@ class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): path = serializers.CharField(max_length=1024, required=False) + owner_username = serializers.ReadOnlyField(source='owner.username') parent = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True) children = serializers.HyperlinkedIdentityField( @@ -32,8 +31,9 @@ class FileBrowserFolderSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ChrisFolder - fields = ('url', 'id', 'creation_date', 'path', 'public', 'parent', 'children', - 'files', 'link_files', 'group_permissions', 'user_permissions', 'owner') + fields = ('url', 'id', 'creation_date', 'path', 'public', 'owner_username', + 'parent', 'children', 'files', 'link_files', 'group_permissions', + 'user_permissions', 'owner') def create(self, validated_data): """ diff --git a/chris_backend/filebrowser/tests/test_views.py b/chris_backend/filebrowser/tests/test_views.py index f5d2173b..02d1a55e 100755 --- a/chris_backend/filebrowser/tests/test_views.py +++ b/chris_backend/filebrowser/tests/test_views.py @@ -81,6 +81,19 @@ def test_filebrowserfolder_create_success(self): content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["path"], f"home/{self.username}/uploads/folder1/folder2") + self.assertFalse(response.data["public"]) + + def test_filebrowserfolder_create_public_status_keeps_unchanged(self): + self.client.login(username=self.username, password=self.password) + post = json.dumps( + {"template": + {"data": [{"name": "public", "value": True}, {"name": "path", + "value": f"home/{self.username}/uploads/folder1/folder3"}]}}) + response = self.client.post(self.create_read_url, data=post, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data["path"], f"home/{self.username}/uploads/folder1/folder3") + self.assertFalse(response.data["public"]) def test_filebrowserfolder_create_failure_unauthenticated(self): response = self.client.post(self.create_read_url, data=self.post, @@ -292,15 +305,65 @@ def test_filebrowserfolder_detail_failure_unauthenticated(self): self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_filebrowserfolder_update_success(self): + # create a folder + owner = User.objects.get(username=self.username) + folder, _ = ChrisFolder.objects.get_or_create( + path=f'home/{self.username}/uploads/test_update', owner=owner) + + # create another folder within the folder + inner_folder_path = f'home/{self.username}/uploads/test_update/inner' + inner_folder, _ = ChrisFolder.objects.get_or_create( + path=inner_folder_path, owner=owner) + + # create a file within the folder + storage_manager = connect_storage(settings) + file_path = f'home/{self.username}/uploads/test_update/update_file.txt' + with io.StringIO("test file") as update_file: + storage_manager.upload_obj(file_path, update_file.read(), + content_type='text/plain') + f = UserFile(owner=owner, parent_folder=folder) + f.fname.name = file_path + f.save() + + # create a link file within the folder + lf_path = f'home/{self.username}/uploads/test_update/SERVICES_PACS.chrislink' + lf = ChrisLinkFile(path='SERVICES/PACS', owner=owner, parent_folder=folder) + lf.save(name='SERVICES_PACS') + + read_update_delete_url = reverse("chrisfolder-detail", + kwargs={"pk": folder.id}) + + new_path = f'home/{self.username}/uploads/test_update_folder/test' + put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}, + {"name": "path", "value": new_path}]}}) + self.client.login(username=self.username, password=self.password) - response = self.client.put(self.read_update_delete_url, data=self.put, + response = self.client.put(read_update_delete_url, data=put, content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["public"],True) + self.assertEqual(response.data["path"], new_path) + folder.refresh_from_db() + + inner_folder.refresh_from_db() + self.assertEqual(inner_folder.path, new_path + '/inner') + self.assertEqual(inner_folder.parent, folder) + + self.assertTrue(storage_manager.obj_exists(new_path + '/update_file.txt')) + self.assertFalse(storage_manager.obj_exists(file_path)) + f.refresh_from_db() + self.assertEqual(f.parent_folder, folder) + + self.assertTrue(storage_manager.obj_exists(new_path + '/SERVICES_PACS.chrislink')) + self.assertFalse(storage_manager.obj_exists(lf_path)) + lf.refresh_from_db() + self.assertEqual(lf.parent_folder, folder) - folder = ChrisFolder.objects.get(path=f'home/{self.username}') folder.remove_public_link() folder.remove_public_access() + folder.delete() def test_filebrowserfolder_update_failure_unauthenticated(self): response = self.client.put(self.read_update_delete_url, data=self.put, @@ -759,12 +822,12 @@ def test_filebrowserfoldergrouppermission_create_success(self): inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) # create a file in the inner folder - self.storage_manager = connect_storage(settings) + storage_manager = connect_storage(settings) upload_path = f'{self.path}/inner/file7.txt' with io.StringIO("test file") as file7: - self.storage_manager.upload_obj(upload_path, file7.read(), - content_type='text/plain') + storage_manager.upload_obj(upload_path, file7.read(), + content_type='text/plain') f = UserFile(owner=user, parent_folder=inner_folder) f.fname.name = upload_path f.save() @@ -956,12 +1019,12 @@ def test_filebrowserfoldergrouppermission_update_success(self): inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) # create a file in the inner folder - self.storage_manager = connect_storage(settings) + storage_manager = connect_storage(settings) upload_path = f'{self.path}/inner/file8.txt' with io.StringIO("test file") as file8: - self.storage_manager.upload_obj(upload_path, file8.read(), - content_type='text/plain') + storage_manager.upload_obj(upload_path, file8.read(), + content_type='text/plain') f = UserFile(owner=user, parent_folder=inner_folder) f.fname.name = upload_path f.save() @@ -1051,12 +1114,12 @@ def test_filebrowserfolderuserpermission_create_success(self): inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) # create a file in the inner folder - self.storage_manager = connect_storage(settings) + storage_manager = connect_storage(settings) upload_path = f'{self.path}/inner/file7.txt' with io.StringIO("test file") as file7: - self.storage_manager.upload_obj(upload_path, file7.read(), - content_type='text/plain') + storage_manager.upload_obj(upload_path, file7.read(), + content_type='text/plain') f = UserFile(owner=user, parent_folder=inner_folder) f.fname.name = upload_path f.save() @@ -1235,12 +1298,12 @@ def test_filebrowserfolderuserpermission_update_success(self): inner_folder = ChrisFolder.objects.create(path=f'{self.path}/inner', owner=user) # create a file in the inner folder - self.storage_manager = connect_storage(settings) + storage_manager = connect_storage(settings) upload_path = f'{self.path}/inner/file8.txt' with io.StringIO("test file") as file8: - self.storage_manager.upload_obj(upload_path, file8.read(), - content_type='text/plain') + storage_manager.upload_obj(upload_path, file8.read(), + content_type='text/plain') f = UserFile(owner=user, parent_folder=inner_folder) f.fname.name = upload_path f.save() @@ -1347,8 +1410,6 @@ def test_filebrowserfolderfile_list_success_public_feed_unauthenticated(self): self.plugin.compute_resources.all()[0]) # create file - self.storage_manager = connect_storage(settings) - file_path = f'{pl_inst.output_folder.path}/file3.txt' with io.StringIO("test file") as file3: self.storage_manager.upload_obj(file_path, file3.read(), @@ -1491,7 +1552,6 @@ def setUp(self): self.read_update_delete_url = reverse("chrisfile-detail", kwargs={"pk": self.file.id}) - self.put = json.dumps({ "template": {"data": [{"name": "public", "value": True}]}}) @@ -1530,8 +1590,6 @@ def test_fileBrowserfile_detail_success_shared_feed(self): pl_inst.feed.save() # create file in the output folder - self.storage_manager = connect_storage(settings) - file_path = f'{pl_inst.output_folder.path}/file4.txt' with io.StringIO("test file") as file4: self.storage_manager.upload_obj(file_path, file4.read(), @@ -1566,8 +1624,6 @@ def test_fileBrowserfile_detail_failure_unauthorized_shared_feed_unauthenticated pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) # create file in the output folder - self.storage_manager = connect_storage(settings) - file_path = f'{pl_inst.output_folder.path}/file5.txt' with io.StringIO("test file") as file5: self.storage_manager.upload_obj(file_path, file5.read(), @@ -1585,10 +1641,18 @@ def test_fileBrowserfile_detail_failure_unauthorized_shared_feed_unauthenticated def test_filebrowserfile_update_success(self): self.client.login(username=self.username, password=self.password) - response = self.client.put(self.read_update_delete_url, data=self.put, + new_file_path = f'home/{self.username}/uploads/mytestfolder/mytestfile.txt' + put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}, + {"name": "new_file_path", "value": new_file_path}]}}) + response = self.client.put(self.read_update_delete_url, data=put, content_type=self.content_type) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["public"],True) + self.assertEqual(response.data["fname"], new_file_path) + self.assertTrue(self.storage_manager.obj_exists(new_file_path)) + self.assertFalse(self.storage_manager.obj_exists(self.upload_path)) + self.file.refresh_from_db() self.file.remove_public_link() self.file.remove_public_access() @@ -1644,8 +1708,6 @@ class FileBrowserFileResourceViewTests(FileBrowserViewTests): def setUp(self): super(FileBrowserFileResourceViewTests, self).setUp() - self.storage_manager = connect_storage(settings) - # create compute resource (compute_resource, tf) = ComputeResource.objects.get_or_create( name="host", compute_url=COMPUTE_RESOURCE_URL) @@ -1718,8 +1780,6 @@ def test_fileBrowserfile_resource_success_shared_file(self): path = f'home/{self.other_username}/uploads' uploads_folder = ChrisFolder.objects.get(path=path) - self.storage_manager = connect_storage(settings) - file_path = f'{path}/file6.txt' with io.StringIO("test file") as file6: self.storage_manager.upload_obj(file_path, file6.read(), @@ -1755,8 +1815,6 @@ def test_fileBrowserfile_resource_failure_unauthorized_shared_feed_unauthenticat pl_inst.feed.save() # create a file in the output folder - self.storage_manager = connect_storage(settings) - file_path = f'{pl_inst.output_folder}/file6.txt' with io.StringIO("test file") as file6: self.storage_manager.upload_obj(file_path, file6.read(), @@ -2316,8 +2374,6 @@ class FileBrowserFolderLinkFileListViewTests(FileBrowserViewTests): def setUp(self): super(FileBrowserFolderLinkFileListViewTests, self).setUp() - self.storage_manager = connect_storage(settings) - # create compute resource (compute_resource, tf) = ComputeResource.objects.get_or_create( name="host", compute_url=COMPUTE_RESOURCE_URL) @@ -2489,8 +2545,11 @@ def setUp(self): self.link_file = ChrisLinkFile(path='SERVICES/PACS', owner=user, parent_folder=self.pl_inst.output_folder) self.link_file.save(name='SERVICES_PACS') - self.read_url = reverse("chrislinkfile-detail", - kwargs={"pk": self.link_file.id}) + + self.read_update_delete_url = reverse("chrislinkfile-detail", + kwargs={"pk": self.link_file.id}) + self.put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}]}}) def tearDown(self): self.link_file.delete() @@ -2498,25 +2557,25 @@ def tearDown(self): def test_fileBrowserlinkfile_detail_success(self): self.client.login(username=self.username, password=self.password) - response = self.client.get(self.read_url) + response = self.client.get(self.read_update_delete_url) self.assertContains(response, self.link_path) def test_fileBrowserlinkfile_detail_success_user_chris(self): self.client.login(username=self.chris_username, password=self.chris_password) - response = self.client.get(self.read_url) + response = self.client.get(self.read_update_delete_url) self.assertContains(response, self.link_path) def test_fileBrowserlinkfile_detail_failure_access_denied(self): self.client.login(username=self.other_username, password=self.other_password) - response = self.client.get(self.read_url) + response = self.client.get(self.read_update_delete_url) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_fileBrowserlinkfile_detail_failure_unauthenticated(self): - response = self.client.get(self.read_url) + response = self.client.get(self.read_update_delete_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_fileBrowserlinkfile_detail_success_public_feed_unauthenticated(self): self.pl_inst.feed.grant_public_access() - response = self.client.get(self.read_url) + response = self.client.get(self.read_update_delete_url) self.assertContains(response, self.link_path) self.pl_inst.feed.remove_public_access() @@ -2543,9 +2602,10 @@ def test_fileBrowserlinkfile_detail_success_shared_feed(self): user = User.objects.get(username=self.username) pl_inst.feed.grant_user_permission(user) - read_url = reverse("chrislinkfile-detail", kwargs={"pk": link_file.id}) + read_update_delete_url = reverse("chrislinkfile-detail", + kwargs={"pk": link_file.id}) self.client.login(username=self.username, password=self.password) - response = self.client.get(read_url) + response = self.client.get(read_update_delete_url) self.assertContains(response, link_path) link_file.delete() @@ -2571,12 +2631,69 @@ def test_fileBrowserlinkfile_detail_failure_unauthorized_shared_feed_unauthentic # share feed pl_inst.feed.grant_user_permission(User.objects.get(username=self.username)) - read_url = reverse("chrislinkfile-detail", kwargs={"pk": link_file.id}) - response = self.client.get(read_url) + read_update_delete_url = reverse("chrislinkfile-detail", + kwargs={"pk": link_file.id}) + response = self.client.get(read_update_delete_url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) link_file.delete() + def test_filebrowserlinkfile_update_success(self): + self.client.login(username=self.username, password=self.password) + new_file_path = f'home/{self.username}/uploads/mytestfolder/SERVICES_PACS.chrislink' + + put = json.dumps({ + "template": {"data": [{"name": "public", "value": True}, + {"name": "new_link_file_path", "value": new_file_path}]}}) + + response = self.client.put(self.read_update_delete_url, data=put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["public"],True) + self.assertEqual(response.data["fname"], new_file_path) + self.assertTrue(self.storage_manager.obj_exists(new_file_path)) + self.assertFalse(self.storage_manager.obj_exists(self.link_path)) + self.link_file.refresh_from_db() + self.link_file.remove_public_link() + self.link_file.remove_public_access() + + def test_filebrowserlinkfile_update_failure_unauthenticated(self): + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserlinkfile_update_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.put(self.read_update_delete_url, data=self.put, + content_type=self.content_type) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_filebrowserlinkfile_delete_success(self): + user = User.objects.get(username=self.username) + + # create link file + lf_path = f'home/{self.username}/uploads/SERVICES_PACS.chrislink' + (parent_folder, _) = ChrisFolder.objects.get_or_create(owner=user, + path=f'home/{self.username}/uploads') + lf = ChrisLinkFile(path='SERVICES/PACS', owner=user, parent_folder=parent_folder) + lf.save(name='SERVICES_PACS') + + read_update_delete_url = reverse("chrislinkfile-detail", + kwargs={"pk": lf.id}) + + self.client.login(username=self.username, password=self.password) + response = self.client.delete(read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse(self.storage_manager.obj_exists(lf_path)) + + def test_filebrowserlinkfile_delete_failure_unauthenticated(self): + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_filebrowserlinkfile_delete_failure_user_access_denied(self): + self.client.login(username=self.other_username, password=self.other_password) + response = self.client.delete(self.read_update_delete_url) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) class FileBrowserLinkFileResourceViewTests(FileBrowserViewTests): """ @@ -2586,8 +2703,6 @@ class FileBrowserLinkFileResourceViewTests(FileBrowserViewTests): def setUp(self): super(FileBrowserLinkFileResourceViewTests, self).setUp() - self.storage_manager = connect_storage(settings) - # create compute resource (compute_resource, tf) = ComputeResource.objects.get_or_create( name="host", compute_url=COMPUTE_RESOURCE_URL) diff --git a/chris_backend/userfiles/serializers.py b/chris_backend/userfiles/serializers.py index d6ed9f02..85428507 100755 --- a/chris_backend/userfiles/serializers.py +++ b/chris_backend/userfiles/serializers.py @@ -122,7 +122,8 @@ def validate_upload_path(self, upload_path): def validate(self, data): """ Overriden to validate that at least one of two fields are in data when - updating a file. + updating a file. Also to validate that required fields are in data on create + and remove the 'public' field if passed. """ if self.instance: # on update if 'public' not in data and 'upload_path' not in data: diff --git a/chris_backend/userfiles/tests/test_serializers.py b/chris_backend/userfiles/tests/test_serializers.py index 97103ada..d21a872d 100755 --- a/chris_backend/userfiles/tests/test_serializers.py +++ b/chris_backend/userfiles/tests/test_serializers.py @@ -140,3 +140,34 @@ def test_validate_upload_path_success(self): upload_path = f'home/{self.username}/uploads/file1.txt' self.assertEqual(userfiles_serializer.validate_upload_path(upload_path), upload_path) + + def test_validate_failure_missing_required_fields_on_create(self): + """ + Test whether overriden validate method validates that required fields are in the + passed data on create. + """ + userfiles_serializer = UserFileSerializer() + + data = {'upload_path': f'home/{self.username}/uploads/file1.txt'} + with self.assertRaises(serializers.ValidationError): + userfiles_serializer.validate(data) + + f = ContentFile('Test file'.encode()) + f.name = 'file1.txt' + data = {'fname': f} + with self.assertRaises(serializers.ValidationError): + userfiles_serializer.validate(data) + + def test_validate_removes_public_field_on_create(self): + """ + Test whether overriden validate method removes the 'public' field from the + passed data on create. + """ + userfiles_serializer = UserFileSerializer() + + f = ContentFile('Test file'.encode()) + f.name = 'file1.txt' + data = {'upload_path': f'home/{self.username}/uploads/file1.txt', 'fname': f, + 'public': True} + validated_data = userfiles_serializer.validate(data) + self.assertNotIn('public', validated_data) From 099885859093ee53d3317a7d812e706a00125c8e Mon Sep 17 00:00:00 2001 From: Jorge Date: Fri, 6 Sep 2024 10:31:37 -0400 Subject: [PATCH 2/2] Fix volume mount path for filesystem storage --- chris_backend/config/settings/local.py | 2 +- docker-compose_noswift.yml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/chris_backend/config/settings/local.py b/chris_backend/config/settings/local.py index 29ce2c00..296cd494 100755 --- a/chris_backend/config/settings/local.py +++ b/chris_backend/config/settings/local.py @@ -101,7 +101,7 @@ MEDIA_ROOT = None if STORAGE_ENV in ('fslink', 'filesystem'): STORAGES['default'] = {'BACKEND': 'django.core.files.storage.FileSystemStorage'} - MEDIA_ROOT = '/var/chris' # local filesystem storage settings + MEDIA_ROOT = '/data' # local filesystem storage settings try: verify_storage_connection( diff --git a/docker-compose_noswift.yml b/docker-compose_noswift.yml index cd992d6c..78a756c7 100755 --- a/docker-compose_noswift.yml +++ b/docker-compose_noswift.yml @@ -6,7 +6,7 @@ # 2. Edit chris_backend/config/settings/local.py, include the configs # # DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' -# MEDIA_ROOT = '/var/chris' +# MEDIA_ROOT = '/data' # # 3. Run ./make.sh -s -i # @@ -38,7 +38,7 @@ services: ENVIRONMENT: local volumes: - ./chris_backend:/opt/app-root/src:z - - ${STOREBASE:?}:/var/chris + - ${STOREBASE:?}:/data environment: - DJANGO_SETTINGS_MODULE=config.settings.local - STORAGE_ENV @@ -60,7 +60,7 @@ services: tty: true # docker run -t volumes: - ./chris_backend:/opt/app-root/src:z - - ${STOREBASE:?}:/var/chris + - ${STOREBASE:?}:/data user: ${UID}:${GID} environment: - DJANGO_SETTINGS_MODULE=config.settings.local @@ -96,7 +96,7 @@ services: ENVIRONMENT: local volumes: - ./chris_backend:/opt/app-root/src:z - - ${STOREBASE:?}:/var/chris + - ${STOREBASE:?}:/data user: ${UID}:${GID} environment: - DJANGO_SETTINGS_MODULE=config.settings.local @@ -138,7 +138,7 @@ services: ENVIRONMENT: local volumes: - ./chris_backend:/opt/app-root/src:z - - ${STOREBASE:?}:/var/chris + - ${STOREBASE:?}:/data user: ${UID}:${GID} environment: - DJANGO_SETTINGS_MODULE=config.settings.local @@ -163,7 +163,7 @@ services: ENVIRONMENT: local volumes: - ./chris_backend:/opt/app-root/src:z - - ${STOREBASE:?}:/var/chris + - ${STOREBASE:?}:/data user: ${UID}:${GID} environment: - DJANGO_SETTINGS_MODULE=config.settings.local