diff --git a/src/common/access_control.py b/src/common/access_control.py index a486b559..8b5ae075 100644 --- a/src/common/access_control.py +++ b/src/common/access_control.py @@ -92,14 +92,10 @@ def user_file_visibility_q(user: AbstractBaseUser) -> Q: def can_edit_user_file(user_file: Any, user: AbstractBaseUser) -> bool: - """Return whether the user may edit/delete a UserFile.""" - if not user or not user.is_authenticated: - return False + """Return whether the user may mutate a UserFile. - return ( - user_file.user_id == user.id - or user_file.institutions.filter( - users=user, - institutionadministration__is_institution_admin=True, - ).exists() - ) + Institution membership controls visibility, but it does not grant + ownership or mutation permissions. Dataset edits, deletion, visibility + changes, and sharing changes are therefore restricted to the owner. + """ + return bool(user and user.is_authenticated and user_file.user_id == user.id) diff --git a/src/frontend/static/frontend/src/components/biomarkers/SharedInstitutionsBiomarker.tsx b/src/frontend/static/frontend/src/components/biomarkers/SharedInstitutionsBiomarker.tsx index ff84cac4..98f9bf6a 100644 --- a/src/frontend/static/frontend/src/components/biomarkers/SharedInstitutionsBiomarker.tsx +++ b/src/frontend/static/frontend/src/components/biomarkers/SharedInstitutionsBiomarker.tsx @@ -1,12 +1,10 @@ import React, { useContext, useEffect, useRef, useState } from 'react' -import { Button, Divider, Grid, GridColumn, Icon, List, ListContent, ListHeader, ListItem, Modal, ModalContent, ModalHeader, Segment, Select, Table } from 'semantic-ui-react' import ky from 'ky' -import { DjangoInstitutionUserLimited } from '../../utils/django_interfaces' -import { PaginatedTable } from '../common/PaginatedTable' -import { TableCellWithTitle } from '../common/TableCellWithTitle' import { CurrentUserContext } from '../Base' import { SemanticListItem } from '../../utils/interfaces' import { getDjangoHeader } from '../../utils/util_functions' +import { useIntl } from 'react-intl' +import { SharedInstitution, SharedInstitutionsModal } from '../common/SharedInstitutionsModal' declare const urlGetUsersCandidatesLimitedBiomarker: string declare const urlGetInstitutionsNonInExperimentBiomarker: string @@ -26,36 +24,6 @@ interface SharedInstitutionsBiomarkerProps extends SharedInstitutionsBiomarkerPr handleChangeConfirmModalState: (setOption: boolean, headerText: string, contentText: string, onConfirm: () => void) => void, } -interface InstitutionUserListProps { - institutionName: string, - institutionId: number, -} - -const InstitutionUserList = (props: InstitutionUserListProps) => { - return ( -
- - headerTitle={props.institutionName + ' users'} - headers={[ - { name: 'User name', serverCodeToSort: 'user__username' as any, width: 3 } - ]} - showSearchInput - searchLabel='User name' - searchPlaceholder='Search by User name' - urlToRetrieveData={urlGetUsersCandidatesLimitedBiomarker + '/' + props.institutionId + '/'} - updateWSKey='update_user_for_institution' - mapFunction={(userCandidate: DjangoInstitutionUserLimited) => { - return ( - - - - ) - }} - /> -
- ) -} - /** * Modal that shows institutions shared to a biomarker * @param props Component props @@ -64,11 +32,12 @@ const InstitutionUserList = (props: InstitutionUserListProps) => { export const SharedInstitutionsBiomarker = (props: SharedInstitutionsBiomarkerProps) => { const [activeInstitution, setActiveInstitution] = useState<{ id: number, name: string }>({ id: 0, name: '' }) const [listOfInstitutionNonPart, setListOfInstitutionNonPart] = useState([]) - const [institutionIdToAdd, setInstitutionIdToAdd] = useState(0) + const [institutionIdToAdd, setInstitutionIdToAdd] = useState(null) const abortController = useRef(new AbortController()) const [institutionList, setInstitutionList] = useState<{ id: number, name: string }[]>([]) const [isLoadingInstitution, setIsLoadingInstitution] = useState(false) const currentUser = useContext(CurrentUserContext) + const intl = useIntl() /** * Function to search institutions that are not in experiment. @@ -120,11 +89,12 @@ export const SharedInstitutionsBiomarker = (props: SharedInstitutionsBiomarkerPr setIsLoadingInstitution(true) const myHeaders = getDjangoHeader() const body = { - institutionId: institutionIdToAdd, + institutionId: Number(institutionIdToAdd), biomarkerId: props.biomarkerId } ky.post(urlShareBiomarkerToInstitution, { headers: myHeaders, signal: abortController.current.signal, json: body }).then((response) => { response.json().then(() => { + setInstitutionIdToAdd(null) InstitutionsistNonInExperiment() usersListInstitution() }).catch((err) => { @@ -165,92 +135,51 @@ export const SharedInstitutionsBiomarker = (props: SharedInstitutionsBiomarkerPr } useEffect(() => { - if (props.biomarkerId) { + if (props.isOpen && props.biomarkerId) { + setInstitutionIdToAdd(null) InstitutionsistNonInExperiment() usersListInstitution() } - }, [props.biomarkerId]) + }, [props.biomarkerId, props.isOpen]) - return ( - props.handleClose()} - open={props.isOpen} - closeIcon={ props.handleClose()} />} - style={{ width: '60%', maxWidth: '1000px' }} - > - Shared institutions - - { - props.user.id === currentUser?.id && - ( - <> - props.onInstitutionToAddChange(value ? String(value) : null)} + /> + + + )} + + + + + +
+ {props.sharedInstitutions.map(institution => ( + props.onSelectInstitution(institution)} + > +
+ + + {institution.name} + + {props.canManage && ( + { + event.stopPropagation() + props.onRemoveInstitution(institution.id) + }} + /> + )} +
+
+ ))} +
+
+
+ + {props.selectedInstitution + ? ( + + ) + : null} + +
+
+
+
+ ) +} diff --git a/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx b/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx index 9b3827dc..858867f1 100644 --- a/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx +++ b/src/frontend/static/frontend/src/components/files-manager/FilesManager.tsx @@ -15,7 +15,7 @@ import { PopupIcons } from '../common/PopupIcons' import { SwitchPublicButton } from '../common/SwitchPublicButton' import { useIntl, IntlShape } from 'react-intl' import { DeleteButton } from '../common/DeleteButton' -import { getTissueDropdownOptions, getTissueIds, TissueLabels } from '../common/TissueLabels' +import { getTissueDropdownOptions, getTissueIds } from '../common/TissueLabels' import { SharedInstitutionsUserFile } from './SharedInstitutionsUserFile' /** Structure returned from the chunk upload service. */ @@ -31,10 +31,9 @@ declare const urlUserFilesCRUD: string declare const urlUserInstitutions: string declare const urlTissuesCRUD: string declare const urlChunkUpload: string -declare const urlChunkUploadComplete: string -declare const downloadFileURL: string -declare const downloadFileHeaders: string -declare const currentUserId: string +declare const urlChunkUploadComplete: string +declare const downloadFileURL: string +declare const downloadFileHeaders: string /** * New File Form fields @@ -843,35 +842,27 @@ class FilesManager extends React.Component {getFileTypeName(userFileRow.file_type)} - - {userFileRow.institutions.length > 0 && ( - i.name).join(', ') } - )} - /> - )} - - - } - tagType={TagType.FILE} - onTagSelect={(tagId) => this.updateUserFileTag(userFileRow, tagId)} - onTagCreated={() => this.getUserTags()} - onTagEdited={() => this.getUserTags()} - onTagDeleted={(deletedTagId) => { - if (this.state.newFile.newTag === deletedTagId) { - this.handleAddFileInputsChange('newTag', null) - } - - this.getUserTags() - }} - /> - + + {userFileRow.is_owner + ? ( + } + tagType={TagType.FILE} + onTagSelect={(tagId) => this.updateUserFileTag(userFileRow, tagId)} + onTagCreated={() => this.getUserTags()} + onTagEdited={() => this.getUserTags()} + onTagDeleted={(deletedTagId) => { + if (this.state.newFile.newTag === deletedTagId) { + this.handleAddFileInputsChange('newTag', null) + } + + this.getUserTags() + }} + /> + ) + : } + {(this.state.sharedInstitutionsByFile[userFileRow.id as number] ?? userFileRow.institutions).length > 0 ? ( @@ -886,9 +877,7 @@ class FilesManager extends React.Component ) : '-'} - - - + { userFileRow.is_public ? ( @@ -918,7 +907,7 @@ class FilesManager extends React.Component { columnName: userFileRow.column_used_as_index } )} /> - {Number(currentUserId) === userFileRow.user.id && ( + {userFileRow.is_owner && ( onClick={() => this.openShareInstitutions(userFileRow)} /> )} - {/* Users can modify or delete own files or the ones which belongs to an - Institution which the user is admin of */} - {userFileRow.is_private_or_institution_admin && ( + {/* Only the dataset owner can modify or delete it. */} + {userFileRow.is_owner && ( <> {/* Shows a edit button if specified */} { const abortController = useRef(new AbortController()) const [availableInstitutions, setAvailableInstitutions] = useState([]) const [sharedInstitutions, setSharedInstitutions] = useState([]) - const [institutionIdToAdd, setInstitutionIdToAdd] = useState(0) + const [institutionIdToAdd, setInstitutionIdToAdd] = useState(null) + const [activeInstitution, setActiveInstitution] = useState(null) const [isLoading, setIsLoading] = useState(false) /** Fetches the institutions available for sharing and those already shared. */ @@ -61,8 +58,10 @@ export const SharedInstitutionsUserFile = (props: Props) => { setSharedInstitutions(shared) props.onInstitutionsChange(shared) - if (!shared.some(institution => institution.id === institutionIdToAdd)) { - setInstitutionIdToAdd(0) + setInstitutionIdToAdd(null) + + if (!shared.some(institution => institution.id === activeInstitution?.id)) { + setActiveInstitution(shared[0] ?? null) } }).catch((err) => { if (!abortController.current.signal.aborted) { @@ -82,7 +81,7 @@ export const SharedInstitutionsUserFile = (props: Props) => { headers: getDjangoHeader(), signal: abortController.current.signal, json: { - institutionId: institutionIdToAdd, + institutionId: Number(institutionIdToAdd), userFileId: props.userFileId } }).then(() => loadInstitutions()) @@ -119,64 +118,37 @@ export const SharedInstitutionsUserFile = (props: Props) => { const canManage = currentUser?.id === props.user.id return ( - `${institutionName} ${intl.formatMessage({ id: 'sharedInstitutions.users' })}`} + userNameLabel={intl.formatMessage({ id: 'sharedInstitutions.userName' })} + searchUserNamePlaceholder={intl.formatMessage({ id: 'sharedInstitutions.searchUserName' })} + removeInstitutionTitle={intl.formatMessage({ id: 'files.manager.sharedInstitutions.remove' })} + isLoading={isLoading} + canManage={canManage} onClose={props.handleClose} - open={props.isOpen} - closeIcon={} - style={{ width: '45%', maxWidth: '650px' }} - > - {intl.formatMessage({ id: 'files.manager.sharedInstitutions.title' })} - - {canManage && ( -
- setInstitutionIdToAdd(Number(value))} - /> - - - ) - } + const canManage = props.user.id === currentUser?.id + const selectedInstitution: SharedInstitution | null = activeInstitution.id ? activeInstitution : null - - - - -
- {institutionList.map(institution => ( - -
-
- setActiveInstitution(institution)}> - - {institution.name} - - -
- {currentUser?.id === props.user.id && - ( - props.handleChangeConfirmModalState(true, intl.formatMessage({ id: 'sharedInstitutions.stopSharing' }), intl.formatMessage({ id: 'sharedInstitutions.confirmStopSharing' }), () => handleRemoveInstitution(institution.id))} - /> - )} -
-
- ))} -
-
-
- - - { - activeInstitution.id - ? ( - - ) - : null - } - -
-
- - + return ( + `${institutionName} ${intl.formatMessage({ id: 'sharedInstitutions.users' })}`} + userNameLabel={intl.formatMessage({ id: 'sharedInstitutions.userName' })} + searchUserNamePlaceholder={intl.formatMessage({ id: 'sharedInstitutions.searchUserName' })} + removeInstitutionTitle={intl.formatMessage({ id: 'sharedInstitutions.removeInstitution' })} + isLoading={isLoadingInstitution} + canManage={canManage} + onClose={props.handleClose} + onInstitutionToAddChange={setInstitutionIdToAdd} + onAddInstitution={() => props.handleChangeConfirmModalState( + true, + intl.formatMessage({ id: 'sharedInstitutions.shareExperiment' }), + intl.formatMessage({ id: 'sharedInstitutions.confirmShare' }), + handleAddInstitution + )} + onSelectInstitution={setActiveInstitution} + onRemoveInstitution={(institutionId) => props.handleChangeConfirmModalState( + true, + intl.formatMessage({ id: 'sharedInstitutions.stopSharing' }), + intl.formatMessage({ id: 'sharedInstitutions.confirmStopSharing' }), + () => handleRemoveInstitution(institutionId) + )} + /> ) } diff --git a/src/frontend/static/frontend/src/css/shared-institutions.css b/src/frontend/static/frontend/src/css/shared-institutions.css new file mode 100644 index 00000000..b5d5b685 --- /dev/null +++ b/src/frontend/static/frontend/src/css/shared-institutions.css @@ -0,0 +1,68 @@ +.shared-institutions-controls { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.shared-institutions-controls .ui.dropdown { + flex: 1; +} + +.shared-institutions-panel { + margin-top: 0 !important; + padding: 1.5rem !important; +} + +.shared-institutions-items { + min-height: 24rem; +} + +.shared-institutions-list.ui.selection.list .list > .shared-institutions-item { + cursor: pointer; + border-left: 3px solid transparent; + padding: 0.75em 0.85em; + transition: background-color 0.15s ease, border-left-color 0.15s ease; +} + +.shared-institutions-list.ui.selection.list .list > .shared-institutions-item:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +.shared-institutions-list.ui.selection.list .list > .shared-institutions-item.active { + background-color: rgba(33, 133, 208, 0.12); + border-left: 3px solid #2185d0; +} + +.shared-institutions-item-content { + display: flex; + align-items: center; + min-height: 2.25rem; +} + +.shared-institutions-item-content .content { + flex: 1; +} + +.shared-institutions-users { + padding-right: 0.5rem; +} + +.shared-institutions-users-column { + padding-left: 2.5rem !important; +} + +@media (max-width: 767px) { + .shared-institutions-controls { + align-items: stretch; + flex-direction: column; + } + + .shared-institutions-controls .ui.button { + align-self: flex-start; + } + + .shared-institutions-users-column { + padding-left: 1rem !important; + } +} diff --git a/src/frontend/static/frontend/src/utils/django_interfaces.ts b/src/frontend/static/frontend/src/utils/django_interfaces.ts index 3c7c8e66..bb08cc87 100644 --- a/src/frontend/static/frontend/src/utils/django_interfaces.ts +++ b/src/frontend/static/frontend/src/utils/django_interfaces.ts @@ -240,7 +240,7 @@ interface DjangoUserFile extends DjangoSimpleUserFile { tag: DjangoTag, upload_date?: string, institutions: DjangoInstitutionSimple[], - is_private_or_institution_admin: boolean, + is_owner: boolean, number_of_rows: number, number_of_samples: number, contains_nan_values: boolean, diff --git a/src/frontend/templates/frontend/base.html b/src/frontend/templates/frontend/base.html index 7c8b6205..256bf224 100644 --- a/src/frontend/templates/frontend/base.html +++ b/src/frontend/templates/frontend/base.html @@ -6,7 +6,7 @@ - Multiomix{% block title %}{% endblock %} + Multiomix {% block title %}{% endblock %} {# Favicon #} @@ -72,4 +72,4 @@ {% block js %}{% endblock %} - \ No newline at end of file + diff --git a/src/frontend/templates/frontend/datasets.html b/src/frontend/templates/frontend/datasets.html index 9e2c82ad..02917b90 100644 --- a/src/frontend/templates/frontend/datasets.html +++ b/src/frontend/templates/frontend/datasets.html @@ -19,6 +19,7 @@ const downloadFileHeaders = "{% url 'user_file_headers' %}" const urlPostSwitchPublicView = "{% url 'switch-userFile-public-view' %}" const urlGetInstitutionsNonInUserFile = "{% url 'institution-non-user-files-list' %}" + const urlGetUsersCandidatesLimited = "{% url 'user_candidates_limited_institution' %}" const urlShareUserFileToInstitution = "{% url 'share-user-file-to-institution' %}" const urlGetSharedInstitutionUserFile = "{% url 'shared-institutions-user-file' %}" const urlPostRemoveInstitutionUserFile = "{% url 'remove-institution-user-file' %}" diff --git a/src/user_files/serializers.py b/src/user_files/serializers.py index 72ba441e..1181b403 100644 --- a/src/user_files/serializers.py +++ b/src/user_files/serializers.py @@ -77,18 +77,14 @@ def to_representation(self, instance): read_only=True ).data - # Check if user can delete this instance (is owner or admin of one of the Dataset's institutions) + # Keep mutation controls owner-only. Institution membership only gives + # read access to datasets shared with that institution. request = self.context.get('request') if request is not None: user = request.user - is_private = instance.user == user - is_institution_admin = instance.institutions.filter( - institutionadministration__user=user, - institutionadministration__is_institution_admin=True - ).exists() - data['is_private_or_institution_admin'] = is_private or is_institution_admin + data['is_owner'] = instance.user == user else: - data['is_private_or_institution_admin'] = False + data['is_owner'] = False return data def create(self, validated_data): diff --git a/src/user_files/tests/test_owner_permissions.py b/src/user_files/tests/test_owner_permissions.py new file mode 100644 index 00000000..f62b6723 --- /dev/null +++ b/src/user_files/tests/test_owner_permissions.py @@ -0,0 +1,134 @@ +import os + +from django.contrib.auth.models import User +from django.test import TestCase +from rest_framework.test import APIClient + +from common.tests_utils import create_user_file +from institutions.models import Institution, InstitutionAdministration +from user_files.models_choices import FileType + + +class UserFileOwnerPermissionsTest(TestCase): + """Ensure institutional visibility does not grant dataset mutation rights.""" + + def setUp(self): + """Create an owner, an institution administrator, and a member.""" + self.owner = User.objects.create_user(username='dataset_owner') + self.institution_admin = User.objects.create_user(username='institution_admin') + self.institution_member = User.objects.create_user(username='institution_member') + + self.institution = Institution.objects.create(name='Shared institution') + InstitutionAdministration.objects.create( + institution=self.institution, + user=self.owner, + ) + InstitutionAdministration.objects.create( + institution=self.institution, + user=self.institution_admin, + is_institution_admin=True, + ) + InstitutionAdministration.objects.create( + institution=self.institution, + user=self.institution_member, + ) + + file_path = os.path.join(os.path.dirname(__file__), 'tests_files', 'Data with dots.csv') + self.user_file = create_user_file(file_path, 'Shared dataset', FileType.MRNA, self.owner) + self.user_file.institutions.add(self.institution) + self.client = APIClient() + + def test_is_owner_replaces_legacy_permission_flag(self): + """The API exposes ownership without the deprecated combined flag.""" + self.client.force_authenticate(user=self.institution_admin) + admin_response = self.client.get(f'/user-files/{self.user_file.pk}/') + + self.assertEqual(admin_response.status_code, 200) + self.assertFalse(admin_response.data['is_owner']) + self.assertNotIn('is_private_or_institution_admin', admin_response.data) + + self.client.force_authenticate(user=self.owner) + owner_response = self.client.get(f'/user-files/{self.user_file.pk}/') + + self.assertEqual(owner_response.status_code, 200) + self.assertTrue(owner_response.data['is_owner']) + self.assertNotIn('is_private_or_institution_admin', owner_response.data) + + def test_institution_admin_can_read_but_cannot_update_or_delete(self): + """Institution administrators retain read access but cannot mutate a dataset.""" + self.client.force_authenticate(user=self.institution_admin) + + read_response = self.client.get(f'/user-files/{self.user_file.pk}/') + update_response = self.client.patch( + f'/user-files/{self.user_file.pk}/', + data={ + 'name': 'Changed by administrator', + 'file_type': FileType.MRNA.value, + 'is_cpg_site_id': False, + }, + format='json', + ) + delete_response = self.client.delete(f'/user-files/{self.user_file.pk}/') + toggle_response = self.client.post( + '/user-files/switch-file-public-view/', + data={'userFileId': self.user_file.pk}, + format='json', + ) + + self.assertEqual(read_response.status_code, 200) + self.assertEqual(update_response.status_code, 404) + self.assertEqual(delete_response.status_code, 404) + self.assertEqual(toggle_response.status_code, 403) + self.user_file.refresh_from_db() + self.assertEqual(self.user_file.name, 'Shared dataset') + + def test_only_owner_can_update_delete_and_manage_sharing(self): + """The owner can perform the complete dataset management workflow.""" + extra_institution = Institution.objects.create(name='Owner institution') + InstitutionAdministration.objects.create( + institution=extra_institution, + user=self.owner, + ) + self.client.force_authenticate(user=self.owner) + + update_response = self.client.patch( + f'/user-files/{self.user_file.pk}/', + data={ + 'name': 'Updated dataset', + 'file_type': FileType.MRNA.value, + 'is_cpg_site_id': False, + }, + format='json', + ) + share_response = self.client.post( + '/user-files/share-to-institution', + data={ + 'userFileId': self.user_file.pk, + 'institutionId': extra_institution.pk, + }, + format='json', + ) + + self.assertEqual(update_response.status_code, 200) + self.assertEqual(share_response.status_code, 200) + self.assertTrue(self.user_file.institutions.filter(pk=extra_institution.pk).exists()) + + remove_response = self.client.post( + '/user-files/remove-institution', + data={ + 'userFileId': self.user_file.pk, + 'institutionId': extra_institution.pk, + }, + format='json', + ) + toggle_response = self.client.post( + '/user-files/switch-file-public-view/', + data={'userFileId': self.user_file.pk}, + format='json', + ) + + self.assertEqual(remove_response.status_code, 200) + self.assertEqual(toggle_response.status_code, 200) + + delete_response = self.client.delete(f'/user-files/{self.user_file.pk}/') + self.assertEqual(delete_response.status_code, 204) diff --git a/src/user_files/views.py b/src/user_files/views.py index 17f3b58f..f4b95073 100644 --- a/src/user_files/views.py +++ b/src/user_files/views.py @@ -11,7 +11,7 @@ from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.views import APIView from common.pagination import StandardResultsSetPagination -from common.access_control import can_edit_shared_resource, can_view_user_file, user_file_visibility_q +from common.access_control import can_edit_user_file, can_view_user_file, user_file_visibility_q from institutions.models import Institution from institutions.serializers import InstitutionSimpleSerializer from user_files.serializers import UserFileSerializer, UserFileWithoutFileObjSerializer @@ -73,16 +73,13 @@ def get_an_user_file(user: AbstractBaseUser, user_file_pk: int) -> UserFile: return get_object_or_404(queryset, pk=user_file_pk) -def get_own_or_as_admin_user_files(user: AbstractBaseUser): +def get_own_user_files(user: AbstractBaseUser): """ - Returns only the User's Files which were uploaded by himself or belongs to an Institution which his is the admin of + Returns only the User's Files which were uploaded by himself. @param user: User to retrieve his Datasets @return: User's Files """ - return UserFile.objects.filter(Q(user=user) | ( - Q(institutions__institutionadministration__user=user) - & Q(institutions__institutionadministration__is_institution_admin=True) - )).distinct() + return UserFile.objects.filter(user=user) def get_user_files(user: AbstractBaseUser, public_only: bool, private_only: bool, with_survival_only: bool) -> QuerySet: @@ -157,14 +154,14 @@ def get_queryset(self): class UserFileDetail(generics.RetrieveUpdateDestroyAPIView): """ - REST endpoint: get, modify or delete for UserFile model. A User can modify or delete ONLY the files which were - uploaded by himself or belongs to an Institution which his is the admin of + REST endpoint: get, modify or delete for UserFile model. Only the owner + can modify or delete a UserFile; institutional visibility is read-only. """ def get_queryset(self): if self.request.method == 'GET': return UserFile.objects.filter(user_file_visibility_q(self.request.user)).distinct() - return get_own_or_as_admin_user_files(self.request.user) + return get_own_user_files(self.request.user) serializer_class = UserFileSerializer permission_classes = [permissions.IsAuthenticated] @@ -180,7 +177,7 @@ def get_queryset(self): """Return the current user's institutions not already shared with the dataset.""" user_file_id = self.kwargs.get('user_file_id') user_file = get_object_or_404(UserFile, pk=user_file_id) - if not can_edit_shared_resource(user_file, self.request.user): + if not can_edit_user_file(user_file, self.request.user): raise PermissionDenied('You do not have permission to modify this dataset.') shared_institution_ids = user_file.institutions.values_list('pk', flat=True) @@ -220,7 +217,7 @@ def post(request): ) user_file = get_object_or_404(UserFile, pk=user_file_id) - if not can_edit_shared_resource(user_file, request.user): + if not can_edit_user_file(user_file, request.user): raise PermissionDenied('You do not have permission to modify this dataset.') institution = get_object_or_404( @@ -244,7 +241,7 @@ def post(request): user_file_id = request.data.get('userFileId') user_file = get_object_or_404(UserFile, pk=user_file_id) - if not can_edit_shared_resource(user_file, request.user): + if not can_edit_user_file(user_file, request.user): raise PermissionDenied('You do not have permission to modify this dataset.') institution = get_object_or_404(Institution, pk=institution_id) @@ -293,7 +290,7 @@ def post(request): user_file_id = data.get('userFileId') user_file: UserFile = get_object_or_404(UserFile, id=user_file_id) - if user_file.user.id != request.user.id: + if not can_edit_user_file(user_file, request.user): raise PermissionDenied('You do not have permission to modify this user file.') user_file.is_public = not user_file.is_public