diff --git a/rsconcept/backend/apps/library/serializers/data_access.py b/rsconcept/backend/apps/library/serializers/data_access.py index fc43d7a6..4742ff2e 100644 --- a/rsconcept/backend/apps/library/serializers/data_access.py +++ b/rsconcept/backend/apps/library/serializers/data_access.py @@ -94,9 +94,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer): class UserTargetSerializer(serializers.Serializer): ''' Serializer: Target single User. ''' - user = PKField(many=False, queryset=User.objects.all()) + user = PKField(many=False, queryset=User.objects.all().only('pk')) class UsersListSerializer(serializers.Serializer): ''' Serializer: List of Users. ''' - users = PKField(many=True, queryset=User.objects.all()) + users = PKField(many=True, queryset=User.objects.all().only('pk')) diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index 8482f9c5..26d4a584 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -183,57 +183,6 @@ class TestLibraryViewset(EndpointTester): self.unowned.refresh_from_db() self.assertEqual(self.unowned.location, data['location']) - @decl_endpoint('/api/library/{item}/add-editor', method='patch') - def test_add_editor(self): - time_update = self.owned.time_update - - data = {'user': self.invalid_user} - self.executeBadData(data=data, item=self.owned.pk) - - data = {'user': self.user.pk} - self.executeNotFound(data=data, item=self.invalid_item) - self.executeForbidden(data=data, item=self.unowned.pk) - - self.executeOK(data=data, item=self.owned.pk) - self.owned.refresh_from_db() - self.assertEqual(self.owned.time_update, time_update) - self.assertEqual(self.owned.editors(), [self.user]) - - self.executeOK(data=data) - self.assertEqual(self.owned.editors(), [self.user]) - - data = {'user': self.user2.pk} - self.executeOK(data=data) - self.assertEqual(set(self.owned.editors()), set([self.user, self.user2])) - - - @decl_endpoint('/api/library/{item}/remove-editor', method='patch') - def test_remove_editor(self): - time_update = self.owned.time_update - - data = {'user': self.invalid_user} - self.executeBadData(data=data, item=self.owned.pk) - - data = {'user': self.user.pk} - self.executeNotFound(data=data, item=self.invalid_item) - self.executeForbidden(data=data, item=self.unowned.pk) - - self.executeOK(data=data, item=self.owned.pk) - self.owned.refresh_from_db() - self.assertEqual(self.owned.time_update, time_update) - self.assertEqual(self.owned.editors(), []) - - Editor.add(item=self.owned, user=self.user) - self.executeOK(data=data) - self.assertEqual(self.owned.editors(), []) - - Editor.add(item=self.owned, user=self.user) - Editor.add(item=self.owned, user=self.user2) - data = {'user': self.user2.pk} - self.executeOK(data=data) - self.assertEqual(self.owned.editors(), [self.user]) - - @decl_endpoint('/api/library/{item}/set-editors', method='patch') def test_set_editors(self): time_update = self.owned.time_update diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index cd5fbbd1..0f9eb6dc 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -13,6 +13,7 @@ from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response +from apps.oss.models import OperationSchema from apps.rsform.models import RSForm from apps.rsform.serializers import RSFormParseSerializer from apps.users.models import User @@ -52,8 +53,6 @@ class LibraryViewSet(viewsets.ModelViewSet): 'set_owner', 'set_access_policy', 'set_location', - 'add_editor', - 'remove_editor', 'set_editors' ]: access_level = permissions.ItemOwner @@ -165,29 +164,19 @@ class LibraryViewSet(viewsets.ModelViewSet): item = self._get_item() serializer = s.UserTargetSerializer(data=request.data) serializer.is_valid(raise_exception=True) - new_owner = serializer.validated_data['user'] - m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner) - return Response(status=c.HTTP_200_OK) + new_owner = serializer.validated_data['user'].pk + if new_owner == item.owner_id: + return Response(status=c.HTTP_200_OK) + + with transaction.atomic(): + if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: + owned_schemas = OperationSchema(item).owned_schemas().only('owner') + for schema in owned_schemas: + schema.owner_id = new_owner + m.LibraryItem.objects.bulk_update(owned_schemas, ['owner']) + item.owner_id = new_owner + item.save(update_fields=['owner']) - @extend_schema( - summary='set AccessPolicy for item', - tags=['Library'], - request=s.AccessPolicySerializer, - responses={ - c.HTTP_200_OK: None, - c.HTTP_400_BAD_REQUEST: None, - c.HTTP_403_FORBIDDEN: None, - c.HTTP_404_NOT_FOUND: None - } - ) - @action(detail=True, methods=['patch'], url_path='set-access-policy') - def set_access_policy(self, request: Request, pk): - ''' Endpoint: Set item AccessPolicy. ''' - item = self._get_item() - serializer = s.AccessPolicySerializer(data=request.data) - serializer.is_valid(raise_exception=True) - new_policy = serializer.validated_data['access_policy'] - m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=new_policy) return Response(status=c.HTTP_200_OK) @extend_schema( @@ -208,49 +197,52 @@ class LibraryViewSet(viewsets.ModelViewSet): serializer = s.LocationSerializer(data=request.data) serializer.is_valid(raise_exception=True) location: str = serializer.validated_data['location'] + if location == item.location: + return Response(status=c.HTTP_200_OK) if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff: return Response(status=c.HTTP_403_FORBIDDEN) - m.LibraryItem.objects.filter(pk=item.pk).update(location=location) + + with transaction.atomic(): + if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: + owned_schemas = OperationSchema(item).owned_schemas().only('location') + for schema in owned_schemas: + schema.location = location + m.LibraryItem.objects.bulk_update(owned_schemas, ['location']) + item.location = location + item.save(update_fields=['location']) + return Response(status=c.HTTP_200_OK) @extend_schema( - summary='add editor for item', + summary='set AccessPolicy for item', tags=['Library'], - request=s.UserTargetSerializer, + request=s.AccessPolicySerializer, responses={ c.HTTP_200_OK: None, + c.HTTP_400_BAD_REQUEST: None, c.HTTP_403_FORBIDDEN: None, c.HTTP_404_NOT_FOUND: None } ) - @action(detail=True, methods=['patch'], url_path='add-editor') - def add_editor(self, request: Request, pk): - ''' Endpoint: Add editor for item. ''' + @action(detail=True, methods=['patch'], url_path='set-access-policy') + def set_access_policy(self, request: Request, pk): + ''' Endpoint: Set item AccessPolicy. ''' item = self._get_item() - serializer = s.UserTargetSerializer(data=request.data) + serializer = s.AccessPolicySerializer(data=request.data) serializer.is_valid(raise_exception=True) - new_editor = serializer.validated_data['user'] - m.Editor.add(item=item, user=new_editor) - return Response(status=c.HTTP_200_OK) + new_policy = serializer.validated_data['access_policy'] + if new_policy == item.access_policy: + return Response(status=c.HTTP_200_OK) + + with transaction.atomic(): + if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: + owned_schemas = OperationSchema(item).owned_schemas().only('access_policy') + for schema in owned_schemas: + schema.access_policy = new_policy + m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy']) + item.access_policy = new_policy + item.save(update_fields=['access_policy']) - @extend_schema( - summary='remove editor for item', - tags=['Library'], - request=s.UserTargetSerializer, - responses={ - c.HTTP_200_OK: None, - c.HTTP_403_FORBIDDEN: None, - c.HTTP_404_NOT_FOUND: None - } - ) - @action(detail=True, methods=['patch'], url_path='remove-editor') - def remove_editor(self, request: Request, pk): - ''' Endpoint: Remove editor for item. ''' - item = self._get_item() - serializer = s.UserTargetSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - editor = serializer.validated_data['user'] - m.Editor.remove(item=item, user=editor) return Response(status=c.HTTP_200_OK) @extend_schema( diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 402d62d4..58f31b0c 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -51,6 +51,14 @@ class OperationSchema: ''' Operation substitutions. ''' return Substitution.objects.filter(operation__oss=self.model) + def owned_schemas(self) -> QuerySet[LibraryItem]: + ''' Get QuerySet containing all result schemas owned by current OSS. ''' + return LibraryItem.objects.filter( + producer__oss=self.model, + owner_id=self.model.owner_id, + location=self.model.location + ) + def update_positions(self, data: list[dict]): ''' Update positions. ''' lookup = {x['id']: x for x in data} diff --git a/rsconcept/backend/apps/oss/tests/s_views/__init__.py b/rsconcept/backend/apps/oss/tests/s_views/__init__.py index 10c776a8..71c01380 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/__init__.py +++ b/rsconcept/backend/apps/oss/tests/s_views/__init__.py @@ -1,2 +1,3 @@ ''' Tests for REST API. ''' +from .t_change_attributes import * from .t_oss import * diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py b/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py new file mode 100644 index 00000000..d8f663da --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_views/t_change_attributes.py @@ -0,0 +1,107 @@ +''' Testing API: Change attributes of OSS and RSForms. ''' + +from rest_framework import status + +from apps.library.models import AccessPolicy, LocationHead +from apps.oss.models import Operation, OperationSchema, OperationType +from apps.rsform.models import RSForm +from apps.users.models import User +from shared.EndpointTester import EndpointTester, decl_endpoint + + +class TestChangeAttributes(EndpointTester): + ''' Testing LibraryItem view when OSS is associated with RSForms. ''' + + def setUp(self): + super().setUp() + self.user3 = User.objects.create( + username='UserTest3', + email='anotheranother@test.com', + password='password' + ) + + self.owned = OperationSchema.create( + title='Test', + alias='T1', + owner=self.user, + location=LocationHead.LIBRARY + ) + self.owned_id = self.owned.model.pk + + self.ks1 = RSForm.create( + alias='KS1', + title='Test1', + owner=self.user, + location=LocationHead.USER + ) + self.ks2 = RSForm.create( + alias='KS2', + title='Test2', + owner=self.user2, + location=LocationHead.LIBRARY + ) + + self.operation1 = self.owned.create_operation( + alias='1', + operation_type=OperationType.INPUT, + result=self.ks1.model + ) + self.operation2 = self.owned.create_operation( + alias='2', + operation_type=OperationType.INPUT, + result=self.ks2.model + ) + + self.operation3 = self.owned.create_operation( + alias='3', + operation_type=OperationType.SYNTHESIS + ) + self.owned.execute_operation(self.operation3) + self.operation3.refresh_from_db() + self.ks3 = self.operation3.result + + + @decl_endpoint('/api/library/{item}/set-owner', method='patch') + def test_set_owner(self): + data = {'user': self.user3.pk} + + self.executeOK(data=data, item=self.owned_id) + + self.owned.refresh_from_db() + self.ks1.refresh_from_db() + self.ks2.refresh_from_db() + self.ks3.refresh_from_db() + self.assertEqual(self.owned.model.owner, self.user3) + self.assertEqual(self.ks1.model.owner, self.user) + self.assertEqual(self.ks2.model.owner, self.user2) + self.assertEqual(self.ks3.owner, self.user3) + + @decl_endpoint('/api/library/{item}/set-location', method='patch') + def test_set_location(self): + data = {'location': '/U/temp'} + + self.executeOK(data=data, item=self.owned_id) + + self.owned.refresh_from_db() + self.ks1.refresh_from_db() + self.ks2.refresh_from_db() + self.ks3.refresh_from_db() + self.assertEqual(self.owned.model.location, data['location']) + self.assertNotEqual(self.ks1.model.location, data['location']) + self.assertNotEqual(self.ks2.model.location, data['location']) + self.assertEqual(self.ks3.location, data['location']) + + @decl_endpoint('/api/library/{item}/set-access-policy', method='patch') + def test_set_access_policy(self): + data = {'access_policy': AccessPolicy.PROTECTED} + + self.executeOK(data=data, item=self.owned_id) + + self.owned.refresh_from_db() + self.ks1.refresh_from_db() + self.ks2.refresh_from_db() + self.ks3.refresh_from_db() + self.assertEqual(self.owned.model.access_policy, data['access_policy']) + self.assertNotEqual(self.ks1.model.access_policy, data['access_policy']) + self.assertNotEqual(self.ks2.model.access_policy, data['access_policy']) + self.assertEqual(self.ks3.access_policy, data['access_policy']) diff --git a/rsconcept/backend/project/settings.py b/rsconcept/backend/project/settings.py index 32e1d5e6..80b7512b 100644 --- a/rsconcept/backend/project/settings.py +++ b/rsconcept/backend/project/settings.py @@ -250,9 +250,8 @@ LOGGING = { 'root': { 'handlers': ['console'], 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO') - }, + } } - if len(sys.argv) > 1 and sys.argv[1] == 'test': logging.disable(logging.CRITICAL) diff --git a/rsconcept/backend/shared/EndpointTester.py b/rsconcept/backend/shared/EndpointTester.py index 27780aa9..cde837f5 100644 --- a/rsconcept/backend/shared/EndpointTester.py +++ b/rsconcept/backend/shared/EndpointTester.py @@ -1,4 +1,7 @@ ''' Utils: base tester class for endpoints. ''' +import logging + +from django.db import connection from rest_framework import status from rest_framework.test import APIClient, APIRequestFactory, APITestCase @@ -40,6 +43,9 @@ class EndpointTester(APITestCase): self.client = APIClient() self.client.force_authenticate(user=self.user) + self.logger = logging.getLogger('django.db.backends') + self.logger.setLevel(logging.DEBUG) + def setUpFullUsers(self): self.factory = APIRequestFactory() self.user = User.objects.create_user( @@ -71,6 +77,16 @@ class EndpointTester(APITestCase): def logout(self): self.client.logout() + def start_db_log(self): + ''' Warning! Do not use this second time before calling stop_db_log. ''' + ''' Warning! Do not forget to enable global logging in settings. ''' + logging.disable(logging.NOTSET) + connection.force_debug_cursor = True + + def stop_db_log(self): + connection.force_debug_cursor = False + logging.disable(logging.CRITICAL) + def set_params(self, **kwargs): ''' Given named argument values resolve current endpoint_mask. ''' if self.endpoint_mask and len(kwargs) > 0: diff --git a/rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx b/rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx index 435753ca..cf5480c2 100644 --- a/rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx +++ b/rsconcept/frontend/src/components/select/MiniSelectorOSS.tsx @@ -17,25 +17,36 @@ interface MiniSelectorOSSProps { function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) { const ossMenu = useDropdown(); + + function onToggle(event: CProps.EventMouse) { + if (items.length > 1) { + ossMenu.toggle(); + } else { + onSelect(event, items[0]); + } + } + return (