diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/apps/rsform/messages.py index f40636b6..9469bbdb 100644 --- a/rsconcept/backend/apps/rsform/messages.py +++ b/rsconcept/backend/apps/rsform/messages.py @@ -60,3 +60,7 @@ def invalidPosition(): def constituentaNoStructure(): return 'Указанная конституента не обладает теоретико-множественной типизацией' + + +def missingFile(): + return 'Отсутствует прикрепленный файл' diff --git a/rsconcept/backend/apps/rsform/permissions.py b/rsconcept/backend/apps/rsform/permissions.py index 9c9a4a81..7a163dec 100644 --- a/rsconcept/backend/apps/rsform/permissions.py +++ b/rsconcept/backend/apps/rsform/permissions.py @@ -57,6 +57,8 @@ class ItemEditor(ItemOwner): ''' Item permission: Editor or higher. ''' def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: + if request.user.is_anonymous: + return False item = _extract_item(obj) if m.Editor.objects.filter( item=item, @@ -69,6 +71,9 @@ class ItemEditor(ItemOwner): class ItemAnyone(ItemEditor): ''' Item permission: Anyone if public. ''' + def has_permission(self, request: Request, view: APIView) -> bool: + return True + def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool: item = _extract_item(obj) if item.access_policy == m.AccessPolicy.PUBLIC: diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py index 921ef840..12c2a4f5 100644 --- a/rsconcept/backend/apps/rsform/serializers/__init__.py +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -20,7 +20,7 @@ from .data_access import ( CstSubstituteSerializer, CstTargetSerializer, InlineSynthesisSerializer, - LibraryItemBase, + LibraryItemBaseSerializer, LibraryItemCloneSerializer, LibraryItemSerializer, RSFormParseSerializer, diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 1e63b952..2861eff7 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -13,13 +13,13 @@ from .basics import CstParseSerializer from .io_pyconcept import PyConceptAdapter -class LibraryItemBase(serializers.ModelSerializer): +class LibraryItemBaseSerializer(serializers.ModelSerializer): ''' Serializer: LibraryItem entry full access. ''' class Meta: ''' serializer metadata. ''' model = LibraryItem fields = '__all__' - read_only_fields = ('id', 'item_type') + read_only_fields = ('id',) class LibraryItemSerializer(serializers.ModelSerializer): @@ -31,6 +31,16 @@ class LibraryItemSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'item_type', 'owner', 'location', 'access_policy') +class LibraryItemCloneSerializer(serializers.ModelSerializer): + ''' Serializer: LibraryItem cloning. ''' + items = PKField(many=True, required=False, queryset=Constituenta.objects.all()) + + class Meta: + ''' serializer metadata. ''' + model = LibraryItem + exclude = ['id', 'item_type', 'owner'] + + class VersionSerializer(serializers.ModelSerializer): ''' Serializer: Version data. ''' class Meta: @@ -220,7 +230,7 @@ class RSFormSerializer(serializers.ModelSerializer): validated_data=new_cst.validated_data ) - loaded_item = LibraryItemBase(data=data) + loaded_item = LibraryItemBaseSerializer(data=data) loaded_item.is_valid(raise_exception=True) loaded_item.update( instance=cast(LibraryItem, self.instance), @@ -337,11 +347,6 @@ class CstListSerializer(serializers.Serializer): return attrs -class LibraryItemCloneSerializer(LibraryItemBase): - ''' Serializer: LibraryItem cloning. ''' - items = PKField(many=True, required=False, queryset=Constituenta.objects.all()) - - class CstMoveSerializer(CstListSerializer): ''' Serializer: Change constituenta position. ''' move_to = serializers.IntegerField() diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py index e560f4e1..3c53d80e 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_library.py @@ -45,10 +45,28 @@ class TestLibraryViewset(EndpointTester): @decl_endpoint('/api/library', method='post') def test_create(self): - data = {'title': 'Title'} + data = { + 'title': 'Title', + 'alias': 'alias', + } + self.executeBadData(data) + + data = { + 'item_type': LibraryItemType.OPERATIONS_SCHEMA, + 'title': 'Title', + 'alias': 'alias', + 'access_policy': AccessPolicy.PROTECTED, + 'visible': False, + 'read_only': True + } response = self.executeCreated(data) - self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['owner'], self.user.pk) + self.assertEqual(response.data['item_type'], data['item_type']) + self.assertEqual(response.data['title'], data['title']) + self.assertEqual(response.data['alias'], data['alias']) + self.assertEqual(response.data['access_policy'], data['access_policy']) + self.assertEqual(response.data['visible'], data['visible']) + self.assertEqual(response.data['read_only'], data['read_only']) self.logout() data = {'title': 'Title2'} @@ -273,6 +291,16 @@ class TestLibraryViewset(EndpointTester): self.assertFalse(response_contains(response, self.unowned)) self.assertFalse(response_contains(response, self.owned)) + @decl_endpoint('/api/library', method='get') + def test_library_get(self): + non_schema = LibraryItem.objects.create( + item_type=LibraryItemType.OPERATIONS_SCHEMA, + title='Test4' + ) + response = self.executeOK() + self.assertTrue(response_contains(response, non_schema)) + self.assertTrue(response_contains(response, self.unowned)) + self.assertTrue(response_contains(response, self.owned)) @decl_endpoint('/api/library/all', method='get') def test_retrieve_all(self): diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index 3e2a52b1..dc735490 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -25,27 +25,17 @@ class TestRSFormViewset(EndpointTester): def setUp(self): super().setUp() - self.schema = RSForm.create(title='Test', alias='T1', owner=self.user) - self.schema_id = self.schema.item.pk + self.owned = RSForm.create(title='Test', alias='T1', owner=self.user) + self.owned_id = self.owned.item.pk self.unowned = RSForm.create(title='Test2', alias='T2') self.unowned_id = self.unowned.item.pk + self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) + self.private_id = self.private.item.pk @decl_endpoint('/api/rsforms/create-detailed', method='post') def test_create_rsform_file(self): work_dir = os.path.dirname(os.path.abspath(__file__)) - with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: - data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'} - response = self.client.post(self.endpoint, data=data, format='multipart') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data['owner'], self.user.pk) - self.assertEqual(response.data['title'], 'Test123') - self.assertEqual(response.data['alias'], 'ks1') - self.assertEqual(response.data['comment'], '123') - - - @decl_endpoint('/api/rsforms/create-detailed', method='post') - def test_create_rsform_json(self): data = { 'title': 'Test123', 'comment': '123', @@ -54,17 +44,20 @@ class TestRSFormViewset(EndpointTester): 'access_policy': AccessPolicy.PROTECTED, 'visible': False } - response = self.executeCreated(data) + self.executeBadData(data) + + with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: + data['file'] = file + response = self.client.post(self.endpoint, data=data, format='multipart') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['alias'], data['alias']) - self.assertEqual(response.data['location'], data['location']) - self.assertEqual(response.data['access_policy'], data['access_policy']) - self.assertEqual(response.data['visible'], data['visible']) + self.assertEqual(response.data['comment'], data['comment']) @decl_endpoint('/api/rsforms', method='get') - def test_list(self): + def test_list_rsforms(self): non_schema = LibraryItem.objects.create( item_type=LibraryItemType.OPERATIONS_SCHEMA, title='Test3' @@ -72,38 +65,41 @@ class TestRSFormViewset(EndpointTester): response = self.executeOK() self.assertFalse(response_contains(response, non_schema)) self.assertTrue(response_contains(response, self.unowned.item)) - self.assertTrue(response_contains(response, self.schema.item)) - - response = self.client.get('/api/library') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertTrue(response_contains(response, non_schema)) - self.assertTrue(response_contains(response, self.unowned.item)) - self.assertTrue(response_contains(response, self.schema.item)) + self.assertTrue(response_contains(response, self.owned.item)) @decl_endpoint('/api/rsforms/{item}/contents', method='get') def test_contents(self): - schema = RSForm.create(title='Title1') - schema.insert_new('X1') - self.executeOK(item=schema.item.pk) + response = self.executeOK(item=self.owned_id) + self.assertEqual(response.data['owner'], self.owned.item.owner.pk) + self.assertEqual(response.data['title'], self.owned.item.title) + self.assertEqual(response.data['alias'], self.owned.item.alias) + self.assertEqual(response.data['location'], self.owned.item.location) + self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) + self.assertEqual(response.data['visible'], self.owned.item.visible) @decl_endpoint('/api/rsforms/{item}/details', method='get') def test_details(self): - schema = RSForm.create(title='Test', owner=self.user) - x1 = schema.insert_new( + x1 = self.owned.insert_new( alias='X1', term_raw='человек', term_resolved='человек' ) - x2 = schema.insert_new( + x2 = self.owned.insert_new( alias='X2', term_raw='@{X1|plur}', term_resolved='люди' ) - response = self.executeOK(item=schema.item.pk) - self.assertEqual(response.data['title'], 'Test') + response = self.executeOK(item=self.owned_id) + self.assertEqual(response.data['owner'], self.owned.item.owner.pk) + self.assertEqual(response.data['title'], self.owned.item.title) + self.assertEqual(response.data['alias'], self.owned.item.alias) + self.assertEqual(response.data['location'], self.owned.item.location) + self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) + self.assertEqual(response.data['visible'], self.owned.item.visible) + self.assertEqual(len(response.data['items']), 2) self.assertEqual(response.data['items'][0]['id'], x1.pk) self.assertEqual(response.data['items'][0]['parse']['status'], 'verified') @@ -115,13 +111,20 @@ class TestRSFormViewset(EndpointTester): self.assertEqual(response.data['subscribers'], [self.user.pk]) self.assertEqual(response.data['editors'], []) + self.executeOK(item=self.unowned_id) + self.executeForbidden(item=self.private_id) + + self.logout() + self.executeOK(item=self.owned_id) + self.executeOK(item=self.unowned_id) + self.executeForbidden(item=self.private_id) + @decl_endpoint('/api/rsforms/{item}/check', method='post') def test_check(self): - schema = RSForm.create(title='Test') - schema.insert_new('X1') + self.owned.insert_new('X1') data = {'expression': 'X1=X1'} - response = self.executeOK(data, item=schema.item.pk) + response = self.executeOK(data, item=self.owned_id) self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['syntax'], 'math') self.assertEqual(response.data['astText'], '[=[X1][X1]]') @@ -133,14 +136,13 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/resolve', method='post') def test_resolve(self): - schema = RSForm.create(title='Test') - x1 = schema.insert_new( + x1 = self.owned.insert_new( alias='X1', term_resolved='синий слон' ) data = {'text': '@{1|редкий} @{X1|plur,datv}'} - response = self.executeOK(data, item=schema.item.pk) + response = self.executeOK(data, item=self.owned_id) self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') self.assertEqual(response.data['output'], 'редким синим слонам') self.assertEqual(len(response.data['refs']), 2) @@ -190,10 +192,10 @@ class TestRSFormViewset(EndpointTester): data = {'alias': 'X3', 'cst_type': CstType.BASE} self.executeForbidden(data, item=self.unowned_id) - self.schema.insert_new('X1') - x2 = self.schema.insert_new('X2') + self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') - response = self.executeCreated(data, item=self.schema_id) + response = self.executeCreated(data, item=self.owned_id) self.assertEqual(response.data['new_cst']['alias'], 'X3') x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x3.order, 3) @@ -205,7 +207,7 @@ class TestRSFormViewset(EndpointTester): 'term_raw': 'test', 'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}] } - response = self.executeCreated(data, item=self.schema_id) + response = self.executeCreated(data, item=self.owned_id) self.assertEqual(response.data['new_cst']['alias'], data['alias']) x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x4.order, 3) @@ -215,7 +217,7 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch') def test_rename_constituenta(self): - x1 = self.schema.insert_new( + x1 = self.owned.insert_new( alias='X1', convention='Test', term_raw='Test1', @@ -223,7 +225,7 @@ class TestRSFormViewset(EndpointTester): term_forms=[{'text': 'form1', 'tags': 'sing,datv'}] ) x2_2 = self.unowned.insert_new('X2') - x3 = self.schema.insert_new( + x3 = self.owned.insert_new( alias='X3', term_raw='Test3', term_resolved='Test3', @@ -233,15 +235,15 @@ class TestRSFormViewset(EndpointTester): data = {'target': x2_2.pk, 'alias': 'D2', 'cst_type': CstType.TERM} self.executeForbidden(data, item=self.unowned_id) - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) data = {'target': x1.pk, 'alias': x1.alias, 'cst_type': CstType.TERM} - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) data = {'target': x1.pk, 'alias': x3.alias} - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) - d1 = self.schema.insert_new( + d1 = self.owned.insert_new( alias='D1', term_raw='@{X1|plur}', definition_formal='X1' @@ -251,7 +253,7 @@ class TestRSFormViewset(EndpointTester): self.assertEqual(x1.cst_type, CstType.BASE) data = {'target': x1.pk, 'alias': 'D2', 'cst_type': CstType.TERM} - response = self.executeOK(data, item=self.schema_id) + response = self.executeOK(data, item=self.owned_id) self.assertEqual(response.data['new_cst']['alias'], 'D2') self.assertEqual(response.data['new_cst']['cst_type'], CstType.TERM) d1.refresh_from_db() @@ -265,13 +267,13 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') def test_substitute_single(self): - x1 = self.schema.insert_new( + x1 = self.owned.insert_new( alias='X1', term_raw='Test1', term_resolved='Test1', term_forms=[{'text': 'form1', 'tags': 'sing,datv'}] ) - x2 = self.schema.insert_new( + x2 = self.owned.insert_new( alias='X2', term_raw='Test2' ) @@ -279,21 +281,21 @@ class TestRSFormViewset(EndpointTester): data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]} self.executeForbidden(data, item=self.unowned_id) - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]} - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]} - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) - d1 = self.schema.insert_new( + d1 = self.owned.insert_new( alias='D1', term_raw='@{X2|sing,datv}', definition_formal='X1' ) data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]} - response = self.executeOK(data, item=self.schema_id) + response = self.executeOK(data, item=self.owned_id) d1.refresh_from_db() x2.refresh_from_db() self.assertEqual(x2.term_raw, 'Test1') @@ -302,12 +304,12 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') def test_substitute_multiple(self): - self.set_params(item=self.schema_id) - x1 = self.schema.insert_new('X1') - x2 = self.schema.insert_new('X2') - d1 = self.schema.insert_new('D1') - d2 = self.schema.insert_new('D2') - d3 = self.schema.insert_new( + self.set_params(item=self.owned_id) + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') + d1 = self.owned.insert_new('D1') + d2 = self.owned.insert_new('D2') + d3 = self.owned.insert_new( alias='D3', definition_formal=r'X1 \ X2' ) @@ -341,7 +343,7 @@ class TestRSFormViewset(EndpointTester): 'transfer_term': True } ]} - response = self.executeOK(data, item=self.schema_id) + response = self.executeOK(data, item=self.owned_id) d3.refresh_from_db() self.assertEqual(d3.definition_formal, r'D1 \ D2') @@ -356,7 +358,7 @@ class TestRSFormViewset(EndpointTester): 'definition_formal': '3', 'definition_raw': '4' } - response = self.executeCreated(data, item=self.schema_id) + response = self.executeCreated(data, item=self.owned_id) self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE) self.assertEqual(response.data['new_cst']['convention'], '1') @@ -369,43 +371,43 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/cst-delete-multiple', method='patch') def test_delete_constituenta(self): - self.set_params(item=self.schema_id) + self.set_params(item=self.owned_id) data = {'items': [1337]} self.executeBadData(data) - x1 = self.schema.insert_new('X1') - x2 = self.schema.insert_new('X2') + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') data = {'items': [x1.pk]} response = self.executeOK(data) x2.refresh_from_db() - self.schema.item.refresh_from_db() + self.owned.item.refresh_from_db() self.assertEqual(len(response.data['items']), 1) - self.assertEqual(self.schema.constituents().count(), 1) + self.assertEqual(self.owned.constituents().count(), 1) self.assertEqual(x2.alias, 'X2') self.assertEqual(x2.order, 1) x3 = self.unowned.insert_new('X1') data = {'items': [x3.pk]} - self.executeBadData(data, item=self.schema_id) + self.executeBadData(data, item=self.owned_id) @decl_endpoint('/api/rsforms/{item}/cst-moveto', method='patch') def test_move_constituenta(self): - self.set_params(item=self.schema_id) + self.set_params(item=self.owned_id) data = {'items': [1337], 'move_to': 1} self.executeBadData(data) - x1 = self.schema.insert_new('X1') - x2 = self.schema.insert_new('X2') + x1 = self.owned.insert_new('X1') + x2 = self.owned.insert_new('X2') data = {'items': [x2.pk], 'move_to': 1} response = self.executeOK(data) x1.refresh_from_db() x2.refresh_from_db() - self.assertEqual(response.data['id'], self.schema_id) + self.assertEqual(response.data['id'], self.owned_id) self.assertEqual(x1.order, 2) self.assertEqual(x2.order, 1) @@ -416,14 +418,14 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch') def test_reset_aliases(self): - self.set_params(item=self.schema_id) + self.set_params(item=self.owned_id) response = self.executeOK() - self.assertEqual(response.data['id'], self.schema_id) + self.assertEqual(response.data['id'], self.owned_id) - x2 = self.schema.insert_new('X2') - x1 = self.schema.insert_new('X1') - d11 = self.schema.insert_new('D11') + x2 = self.owned.insert_new('X2') + x1 = self.owned.insert_new('X1') + d11 = self.owned.insert_new('D11') response = self.executeOK() x1.refresh_from_db() @@ -441,43 +443,43 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/load-trs', method='patch') def test_load_trs(self): - self.set_params(item=self.schema_id) - self.schema.item.title = 'Test11' - self.schema.item.save() - x1 = self.schema.insert_new('X1') + self.set_params(item=self.owned_id) + self.owned.item.title = 'Test11' + self.owned.item.save() + x1 = self.owned.insert_new('X1') work_dir = os.path.dirname(os.path.abspath(__file__)) with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: data = {'file': file, 'load_metadata': False} response = self.client.patch(self.endpoint, data=data, format='multipart') - self.schema.item.refresh_from_db() + self.owned.item.refresh_from_db() self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(self.schema.item.title, 'Test11') + self.assertEqual(self.owned.item.title, 'Test11') self.assertEqual(len(response.data['items']), 25) - self.assertEqual(self.schema.constituents().count(), 25) + self.assertEqual(self.owned.constituents().count(), 25) self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) @decl_endpoint('/api/rsforms/{item}/cst-produce-structure', method='patch') def test_produce_structure(self): - self.set_params(item=self.schema_id) - x1 = self.schema.insert_new('X1') - s1 = self.schema.insert_new( + self.set_params(item=self.owned_id) + x1 = self.owned.insert_new('X1') + s1 = self.owned.insert_new( alias='S1', definition_formal='ℬ(X1×X1)' ) - s2 = self.schema.insert_new( + s2 = self.owned.insert_new( alias='S2', definition_formal='invalid' ) - s3 = self.schema.insert_new( + s3 = self.owned.insert_new( alias='S3', definition_formal='X1×(X1×ℬℬ(X1))×ℬ(X1×X1)' ) - a1 = self.schema.insert_new( + a1 = self.owned.insert_new( alias='A1', definition_formal='1=1' ) - f1 = self.schema.insert_new( + f1 = self.owned.insert_new( alias='F10', definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)' ) diff --git a/rsconcept/backend/apps/rsform/views/library.py b/rsconcept/backend/apps/rsform/views/library.py index fd9d2a89..4ce592af 100644 --- a/rsconcept/backend/apps/rsform/views/library.py +++ b/rsconcept/backend/apps/rsform/views/library.py @@ -18,74 +18,22 @@ from .. import permissions from .. import serializers as s -@extend_schema(tags=['Library']) -@extend_schema_view() -class LibraryActiveView(generics.ListAPIView): - ''' Endpoint: Get list of library items available for active user. ''' - permission_classes = (permissions.Anyone,) - serializer_class = s.LibraryItemSerializer - - def get_queryset(self): - if self.request.user.is_anonymous: - return m.LibraryItem.objects.filter( - Q(access_policy=m.AccessPolicy.PUBLIC), - ).filter( - Q(location__startswith=m.LocationHead.COMMON) | - Q(location__startswith=m.LocationHead.LIBRARY) - ).order_by('-time_update') - else: - user = cast(m.User, self.request.user) - # pylint: disable=unsupported-binary-operation - return m.LibraryItem.objects.filter( - ( - Q(access_policy=m.AccessPolicy.PUBLIC) & - ( - Q(location__startswith=m.LocationHead.COMMON) | - Q(location__startswith=m.LocationHead.LIBRARY) - ) - ) | - Q(owner=user) | - Q(editor__editor=user) | - Q(subscription__user=user) - ).distinct().order_by('-time_update') - - -@extend_schema(tags=['Library']) -@extend_schema_view() -class LibraryAdminView(generics.ListAPIView): - ''' Endpoint: Get list of all library items. Admin only ''' - permission_classes = (permissions.GlobalAdmin,) - serializer_class = s.LibraryItemSerializer - - def get_queryset(self): - return m.LibraryItem.objects.all().order_by('-time_update') - - -@extend_schema(tags=['Library']) -@extend_schema_view() -class LibraryTemplatesView(generics.ListAPIView): - ''' Endpoint: Get list of templates. ''' - permission_classes = (permissions.Anyone,) - serializer_class = s.LibraryItemSerializer - - def get_queryset(self): - template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True) - return m.LibraryItem.objects.filter(pk__in=template_ids) - - -# pylint: disable=too-many-ancestors @extend_schema(tags=['Library']) @extend_schema_view() class LibraryViewSet(viewsets.ModelViewSet): ''' Endpoint: Library operations. ''' queryset = m.LibraryItem.objects.all() - serializer_class = s.LibraryItemSerializer filter_backends = (DjangoFilterBackend, filters.OrderingFilter) filterset_fields = ['item_type', 'owner'] ordering_fields = ('item_type', 'owner', 'alias', 'title', 'time_update') ordering = '-time_update' + def get_serializer_class(self): + if self.action == 'create': + return s.LibraryItemBaseSerializer + return s.LibraryItemSerializer + def perform_create(self, serializer): if not self.request.user.is_anonymous and 'owner' not in self.request.POST: return serializer.save(owner=self.request.user) @@ -103,7 +51,7 @@ class LibraryViewSet(viewsets.ModelViewSet): elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']: permission_list = [permissions.GlobalUser] else: - permission_list = [permissions.Anyone] + permission_list = [permissions.ItemAnyone] return [permission() for permission in permission_list] def _get_item(self) -> m.LibraryItem: @@ -308,3 +256,58 @@ class LibraryViewSet(viewsets.ModelViewSet): editors = serializer.validated_data['users'] m.Editor.set(item=item, users=editors) return Response(status=c.HTTP_200_OK) + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryActiveView(generics.ListAPIView): + ''' Endpoint: Get list of library items available for active user. ''' + permission_classes = (permissions.Anyone,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + if self.request.user.is_anonymous: + return m.LibraryItem.objects.filter( + Q(access_policy=m.AccessPolicy.PUBLIC), + ).filter( + Q(location__startswith=m.LocationHead.COMMON) | + Q(location__startswith=m.LocationHead.LIBRARY) + ).order_by('-time_update') + else: + user = cast(m.User, self.request.user) + # pylint: disable=unsupported-binary-operation + return m.LibraryItem.objects.filter( + ( + Q(access_policy=m.AccessPolicy.PUBLIC) & + ( + Q(location__startswith=m.LocationHead.COMMON) | + Q(location__startswith=m.LocationHead.LIBRARY) + ) + ) | + Q(owner=user) | + Q(editor__editor=user) | + Q(subscription__user=user) + ).distinct().order_by('-time_update') + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryAdminView(generics.ListAPIView): + ''' Endpoint: Get list of all library items. Admin only ''' + permission_classes = (permissions.GlobalAdmin,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + return m.LibraryItem.objects.all().order_by('-time_update') + + +@extend_schema(tags=['Library']) +@extend_schema_view() +class LibraryTemplatesView(generics.ListAPIView): + ''' Endpoint: Get list of templates. ''' + permission_classes = (permissions.Anyone,) + serializer_class = s.LibraryItemSerializer + + def get_queryset(self): + template_ids = m.LibraryTemplate.objects.values_list('lib_source', flat=True) + return m.LibraryItem.objects.filter(pk__in=template_ids) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 5a3213f0..feb4438d 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -441,6 +441,7 @@ class TrsImportView(views.APIView): request=s.LibraryItemSerializer, responses={ c.HTTP_201_CREATED: s.LibraryItemSerializer, + c.HTTP_400_BAD_REQUEST: None, c.HTTP_403_FORBIDDEN: None } ) @@ -449,17 +450,9 @@ def create_rsform(request: Request): ''' Endpoint: Create RSForm from user input and/or trs file. ''' owner = cast(m.User, request.user) if not request.user.is_anonymous else None if 'file' not in request.FILES: - serializer = s.LibraryItemBase(data=request.data) - serializer.is_valid(raise_exception=True) - schema = m.RSForm.create( - title=serializer.validated_data['title'], - owner=owner, - alias=serializer.validated_data.get('alias', ''), - comment=serializer.validated_data.get('comment', ''), - visible=serializer.validated_data.get('visible', True), - read_only=serializer.validated_data.get('read_only', False), - access_policy=serializer.validated_data.get('access_policy', m.AccessPolicy.PUBLIC), - location=serializer.validated_data.get('location', m.LocationHead.USER), + return Response( + status=c.HTTP_400_BAD_REQUEST, + data={f'file': msg.missingFile()} ) else: data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) diff --git a/rsconcept/frontend/src/app/Router.tsx b/rsconcept/frontend/src/app/Router.tsx index 44662c4f..62028d26 100644 --- a/rsconcept/frontend/src/app/Router.tsx +++ b/rsconcept/frontend/src/app/Router.tsx @@ -1,6 +1,6 @@ import { createBrowserRouter } from 'react-router-dom'; -import CreateItemPage from '@/pages/CreateRSFormPage'; +import CreateItemPage from '@/pages/CreateItemPage'; import HomePage from '@/pages/HomePage'; import LibraryPage from '@/pages/LibraryPage'; import LoginPage from '@/pages/LoginPage'; diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts index 361ffef7..c5c099cc 100644 --- a/rsconcept/frontend/src/app/backendAPI.ts +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -198,7 +198,7 @@ export function getTemplates(request: FrontPull) { }); } -export function postNewRSForm(request: FrontExchange) { +export function postRSFormFromFile(request: FrontExchange) { AxiosPost({ endpoint: '/api/rsforms/create-detailed', request: request, @@ -210,6 +210,13 @@ export function postNewRSForm(request: FrontExchange) { + AxiosPost({ + endpoint: '/api/library', + request: request + }); +} + export function postCloneLibraryItem(target: string, request: FrontExchange) { AxiosPost({ endpoint: `/api/library/${target}/clone`, diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts index 4a640f77..a2ff397c 100644 --- a/rsconcept/frontend/src/app/urls.ts +++ b/rsconcept/frontend/src/app/urls.ts @@ -44,6 +44,7 @@ export const urls = { help_topic: (topic: string) => `/manuals?topic=${topic}`, schema: (id: number | string, version?: number | string) => `/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''), + oss: (id: number | string) => `/oss/${id}`, schema_props: ({ id, tab, version, active }: SchemaProps) => { const versionStr = version !== undefined ? `v=${version}&` : ''; const activeStr = active !== undefined ? `&active=${active}` : ''; diff --git a/rsconcept/frontend/src/components/DomainIcons.tsx b/rsconcept/frontend/src/components/DomainIcons.tsx index 839b3ecc..61b51c28 100644 --- a/rsconcept/frontend/src/components/DomainIcons.tsx +++ b/rsconcept/frontend/src/components/DomainIcons.tsx @@ -1,4 +1,4 @@ -import { AccessPolicy, LocationHead } from '@/models/library'; +import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; import { CstMatchMode, DependencyMode } from '@/models/miscellaneous'; import { @@ -13,10 +13,12 @@ import { IconGraphInputs, IconGraphOutputs, IconHide, + IconOSS, IconPrivate, IconProps, IconProtected, IconPublic, + IconRSForm, IconSettings, IconShow, IconTemplates, @@ -29,6 +31,15 @@ export interface DomIconProps extends IconProps { value: RequestData; } +export function ItemTypeIcon({ value, size = '1.25rem', className }: DomIconProps) { + switch (value) { + case LibraryItemType.RSFORM: + return ; + case LibraryItemType.OSS: + return ; + } +} + export function PolicyIcon({ value, size = '1.25rem', className }: DomIconProps) { switch (value) { case AccessPolicy.PRIVATE: diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx index 8e4496a9..ad1a2eba 100644 --- a/rsconcept/frontend/src/components/Icons.tsx +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -56,6 +56,8 @@ export { TbBriefcase as IconBusiness } from 'react-icons/tb'; export { VscLibrary as IconLibrary } from 'react-icons/vsc'; export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; export { BiDiamond as IconTemplates } from 'react-icons/bi'; +export { FaRegObjectGroup as IconOSS } from 'react-icons/fa'; +export { RiHexagonLine as IconRSForm } from 'react-icons/ri'; export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu'; diff --git a/rsconcept/frontend/src/components/select/SelectItemType.tsx b/rsconcept/frontend/src/components/select/SelectItemType.tsx new file mode 100644 index 00000000..35d4f466 --- /dev/null +++ b/rsconcept/frontend/src/components/select/SelectItemType.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useCallback } from 'react'; + +import Dropdown from '@/components/ui/Dropdown'; +import useDropdown from '@/hooks/useDropdown'; +import { LibraryItemType } from '@/models/library'; +import { prefixes } from '@/utils/constants'; +import { describeLibraryItemType, labelLibraryItemType } from '@/utils/labels'; + +import { ItemTypeIcon } from '../DomainIcons'; +import DropdownButton from '../ui/DropdownButton'; +import SelectorButton from '../ui/SelectorButton'; + +interface SelectItemTypeProps { + value: LibraryItemType; + onChange: (value: LibraryItemType) => void; + disabled?: boolean; + stretchLeft?: boolean; +} + +function SelectItemType({ value, disabled, stretchLeft, onChange }: SelectItemTypeProps) { + const menu = useDropdown(); + + const handleChange = useCallback( + (newValue: LibraryItemType) => { + menu.hide(); + if (newValue !== value) { + onChange(newValue); + } + }, + [menu, value, onChange] + ); + + return ( +
+ } + text={labelLibraryItemType(value)} + onClick={menu.toggle} + disabled={disabled} + /> + + {Object.values(LibraryItemType).map((item, index) => ( + } + onClick={() => handleChange(item)} + /> + ))} + +
+ ); +} + +export default SelectItemType; diff --git a/rsconcept/frontend/src/components/select/SelectMatchMode.tsx b/rsconcept/frontend/src/components/select/SelectMatchMode.tsx index 620fa33e..fff4951f 100644 --- a/rsconcept/frontend/src/components/select/SelectMatchMode.tsx +++ b/rsconcept/frontend/src/components/select/SelectMatchMode.tsx @@ -34,7 +34,6 @@ function SelectMatchMode({ value, onChange }: SelectMatchModeProps) {
{ const createItem = useCallback( (data: ILibraryCreateData, callback?: DataCallback) => { + const onSuccess = (newSchema: ILibraryItem) => + reloadItems(() => { + if (user && !user.subscriptions.includes(newSchema.id)) { + user.subscriptions.push(newSchema.id); + } + if (callback) callback(newSchema); + }); setError(undefined); - postNewRSForm({ - data: data, - showError: true, - setLoading: setProcessing, - onError: setError, - onSuccess: newSchema => - reloadItems(() => { - if (user && !user.subscriptions.includes(newSchema.id)) { - user.subscriptions.push(newSchema.id); - } - if (callback) callback(newSchema); - }) - }); + if (data.file) { + postRSFormFromFile({ + data: data, + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: onSuccess + }); + } else { + postCreateLibraryItem({ + data: data, + showError: true, + setLoading: setProcessing, + onError: setError, + onSuccess: onSuccess + }); + } }, [reloadItems, user] ); diff --git a/rsconcept/frontend/src/models/library.ts b/rsconcept/frontend/src/models/library.ts index 860b65da..db94d298 100644 --- a/rsconcept/frontend/src/models/library.ts +++ b/rsconcept/frontend/src/models/library.ts @@ -9,7 +9,7 @@ import { UserID } from './user'; */ export enum LibraryItemType { RSFORM = 'rsform', - OPERATIONS_SCHEMA = 'oss' + OSS = 'oss' } /** diff --git a/rsconcept/frontend/src/pages/CreateRSFormPage/CreateItemPage.tsx b/rsconcept/frontend/src/pages/CreateItemPage/CreateItemPage.tsx similarity index 100% rename from rsconcept/frontend/src/pages/CreateRSFormPage/CreateItemPage.tsx rename to rsconcept/frontend/src/pages/CreateItemPage/CreateItemPage.tsx diff --git a/rsconcept/frontend/src/pages/CreateRSFormPage/FormCreateItem.tsx b/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx similarity index 88% rename from rsconcept/frontend/src/pages/CreateRSFormPage/FormCreateItem.tsx rename to rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx index 043f0676..b065eddc 100644 --- a/rsconcept/frontend/src/pages/CreateRSFormPage/FormCreateItem.tsx +++ b/rsconcept/frontend/src/pages/CreateItemPage/FormCreateItem.tsx @@ -9,6 +9,7 @@ import { VisibilityIcon } from '@/components/DomainIcons'; import { IconDownload } from '@/components/Icons'; import InfoError from '@/components/info/InfoError'; import SelectAccessPolicy from '@/components/select/SelectAccessPolicy'; +import SelectItemType from '@/components/select/SelectItemType'; import SelectLocationHead from '@/components/select/SelectLocationHead'; import Button from '@/components/ui/Button'; import Label from '@/components/ui/Label'; @@ -30,6 +31,7 @@ function FormCreateItem() { const { user } = useAuth(); const { createItem, error, setError, processing } = useLibrary(); + const [itemType, setItemType] = useState(LibraryItemType.RSFORM); const [title, setTitle] = useState(''); const [alias, setAlias] = useState(''); const [comment, setComment] = useState(''); @@ -64,7 +66,7 @@ function FormCreateItem() { return; } const data: ILibraryCreateData = { - item_type: LibraryItemType.RSFORM, + item_type: itemType, title: title, alias: alias, comment: comment, @@ -75,9 +77,13 @@ function FormCreateItem() { file: file, fileName: file?.name }; - createItem(data, newSchema => { + createItem(data, newItem => { toast.success('Схема успешно создана'); - router.push(urls.schema(newSchema.id)); + if (itemType == LibraryItemType.RSFORM) { + router.push(urls.schema(newItem.id)); + } else { + router.push(urls.oss(newItem.id)); + } }); } @@ -109,8 +115,8 @@ function FormCreateItem() { /> -
-

Создание концептуальной схемы

+ +

Создание схемы

{fileName ?