diff --git a/rsconcept/backend/apps/rsform/models/Constituenta.py b/rsconcept/backend/apps/rsform/models/Constituenta.py index 03baa666..7f81f04d 100644 --- a/rsconcept/backend/apps/rsform/models/Constituenta.py +++ b/rsconcept/backend/apps/rsform/models/Constituenta.py @@ -95,10 +95,6 @@ class Constituenta(Model): verbose_name = 'Конституента' verbose_name_plural = 'Конституенты' - def get_absolute_url(self): - ''' URL access. ''' - return reverse('constituenta-detail', kwargs={'pk': self.pk}) - def __str__(self) -> str: return f'{self.alias}' diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 4791c0bb..cace4404 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -31,7 +31,7 @@ class CstSerializer(serializers.ModelSerializer): ''' serializer metadata. ''' model = Constituenta fields = '__all__' - read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') + read_only_fields = ('id', 'schema', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') def update(self, instance: Constituenta, validated_data) -> Constituenta: data = validated_data # Note: use alias for better code readability diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py index 0d254249..cda9806b 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_Constituenta.py @@ -20,12 +20,6 @@ class TestConstituenta(TestCase): self.assertEqual(str(cst), testStr) - def test_url(self): - testStr = 'X1' - cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test') - self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.pk}') - - def test_order_not_null(self): with self.assertRaises(IntegrityError): Constituenta.objects.create(alias='X1', schema=self.schema1.model) diff --git a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py index 28333d03..187bc021 100644 --- a/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py +++ b/rsconcept/backend/apps/rsform/tests/s_models/t_RSForm.py @@ -208,11 +208,11 @@ class TestRSForm(TestCase): definition_formal=x1.alias ) - self.schema.substitute(x1, x2, True) + self.schema.substitute(x1, x2) x2.refresh_from_db() d1.refresh_from_db() self.assertEqual(self.schema.constituents().count(), 2) - self.assertEqual(x2.term_raw, 'Test') + self.assertEqual(x2.term_raw, 'Test2') self.assertEqual(d1.definition_formal, x2.alias) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/__init__.py b/rsconcept/backend/apps/rsform/tests/s_views/__init__.py index 1a564c03..84607134 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/__init__.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/__init__.py @@ -1,6 +1,4 @@ ''' Tests for REST API. ''' from .t_cctext import * -from .t_constituents import * -from .t_operations import * from .t_rsforms import * from .t_rslang import * diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py b/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py deleted file mode 100644 index 24159ea3..00000000 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_constituents.py +++ /dev/null @@ -1,102 +0,0 @@ -''' Testing API: Constituents. ''' -from apps.rsform.models import Constituenta, CstType, RSForm -from shared.EndpointTester import EndpointTester, decl_endpoint - - -class TestConstituentaAPI(EndpointTester): - ''' Testing Constituenta view. ''' - - def setUp(self): - super().setUp() - self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) - self.rsform_unowned = RSForm.create(title='Test2', alias='T2') - self.cst1 = Constituenta.objects.create( - alias='X1', - cst_type=CstType.BASE, - schema=self.rsform_owned.model, - order=1, - convention='Test', - term_raw='Test1', - term_resolved='Test1R', - term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]) - self.cst2 = Constituenta.objects.create( - alias='X2', - cst_type=CstType.BASE, - schema=self.rsform_unowned.model, - order=1, - convention='Test1', - term_raw='Test2', - term_resolved='Test2R' - ) - self.cst3 = Constituenta.objects.create( - alias='X3', - schema=self.rsform_owned.model, - order=2, - term_raw='Test3', - term_resolved='Test3', - definition_raw='Test1', - definition_resolved='Test2' - ) - self.invalid_cst = self.cst3.pk + 1337 - - - @decl_endpoint('/api/constituents/{item}', method='get') - def test_retrieve(self): - self.executeNotFound(item=self.invalid_cst) - response = self.executeOK(item=self.cst1.pk) - self.assertEqual(response.data['alias'], self.cst1.alias) - self.assertEqual(response.data['convention'], self.cst1.convention) - - - @decl_endpoint('/api/constituents/{item}', method='patch') - def test_partial_update(self): - data = {'convention': 'tt'} - self.executeForbidden(data=data, item=self.cst2.pk) - - self.logout() - self.executeForbidden(data=data, item=self.cst1.pk) - - self.login() - response = self.executeOK(data=data, item=self.cst1.pk) - self.cst1.refresh_from_db() - self.assertEqual(response.data['convention'], 'tt') - self.assertEqual(self.cst1.convention, 'tt') - - self.executeOK(data=data, item=self.cst1.pk) - - - @decl_endpoint('/api/constituents/{item}', method='patch') - def test_update_resolved_no_refs(self): - data = { - 'term_raw': 'New term', - 'definition_raw': 'New def' - } - response = self.executeOK(data=data, item=self.cst3.pk) - self.cst3.refresh_from_db() - self.assertEqual(response.data['term_resolved'], 'New term') - self.assertEqual(self.cst3.term_resolved, 'New term') - self.assertEqual(response.data['definition_resolved'], 'New def') - self.assertEqual(self.cst3.definition_resolved, 'New def') - - - @decl_endpoint('/api/constituents/{item}', method='patch') - def test_update_resolved_refs(self): - data = { - 'term_raw': '@{X1|nomn,sing}', - 'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}' - } - response = self.executeOK(data=data, item=self.cst3.pk) - self.cst3.refresh_from_db() - self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) - self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved) - self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1') - self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1') - - - @decl_endpoint('/api/constituents/{item}', method='patch') - def test_readonly_cst_fields(self): - data = {'alias': 'X33', 'order': 10} - response = self.executeOK(data=data, item=self.cst1.pk) - self.assertEqual(response.data['alias'], 'X1') - self.assertEqual(response.data['alias'], self.cst1.alias) - self.assertEqual(response.data['order'], self.cst1.order) diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py b/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py deleted file mode 100644 index 8188bcc5..00000000 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_operations.py +++ /dev/null @@ -1,80 +0,0 @@ -''' Testing API: Operations. ''' -from apps.rsform.models import Constituenta, CstType, RSForm -from shared.EndpointTester import EndpointTester, decl_endpoint - - -class TestInlineSynthesis(EndpointTester): - ''' Testing Operations endpoints. ''' - - - @decl_endpoint('/api/operations/inline-synthesis', method='patch') - def setUp(self): - super().setUp() - self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user) - self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user) - self.unowned = RSForm.create(title='Test3', alias='T3') - - - def test_inline_synthesis_inputs(self): - invalid_id = 1338 - data = { - 'receiver': self.unowned.model.pk, - 'source': self.schema1.model.pk, - 'items': [], - 'substitutions': [] - } - self.executeForbidden(data=data) - - data['receiver'] = invalid_id - self.executeBadData(data=data) - - data['receiver'] = self.schema1.model.pk - data['source'] = invalid_id - self.executeBadData(data=data) - - data['source'] = self.schema1.model.pk - self.executeOK(data=data) - - data['items'] = [invalid_id] - self.executeBadData(data=data) - - - def test_inline_synthesis(self): - ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete - ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2 - ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1 - ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1 - ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete - ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4 - ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2 - ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2 - ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items - - data = { - 'receiver': self.schema1.model.pk, - 'source': self.schema2.model.pk, - 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], - 'substitutions': [ - { - 'original': ks1_x1.pk, - 'substitution': ks2_s1.pk - }, - { - 'original': ks2_x1.pk, - 'substitution': ks1_s1.pk - } - ] - } - response = self.executeOK(data=data) - result = {item['alias']: item for item in response.data['items']} - self.assertEqual(len(result), 6) - self.assertEqual(result['X2']['term_raw'], ks1_x2.term_raw) - self.assertEqual(result['X2']['order'], 1) - self.assertEqual(result['X4']['term_raw'], ks2_x2.term_raw) - self.assertEqual(result['X4']['order'], 2) - self.assertEqual(result['S1']['term_raw'], ks2_x1.term_raw) - self.assertEqual(result['S2']['term_raw'], ks2_s1.term_raw) - self.assertEqual(result['S1']['definition_formal'], 'X2') - self.assertEqual(result['S2']['definition_formal'], 'X4×X4') - self.assertEqual(result['D1']['definition_formal'], r'S1\S2\X2') - self.assertEqual(result['D2']['definition_formal'], r'S2\S1\X4') 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 054e1d43..e8e8ecd5 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -482,3 +482,172 @@ class TestRSFormViewset(EndpointTester): self.assertEqual(len(items), 2) self.assertEqual(items[0]['order'], f1.order + 1) self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])') + + +class TestConstituentaAPI(EndpointTester): + ''' Testing Constituenta view. ''' + + def setUp(self): + super().setUp() + self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) + self.rsform_unowned = RSForm.create(title='Test2', alias='T2') + self.cst1 = Constituenta.objects.create( + alias='X1', + cst_type=CstType.BASE, + schema=self.rsform_owned.model, + order=1, + convention='Test', + term_raw='Test1', + term_resolved='Test1R', + term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]) + self.cst2 = Constituenta.objects.create( + alias='X2', + cst_type=CstType.BASE, + schema=self.rsform_unowned.model, + order=1, + convention='Test1', + term_raw='Test2', + term_resolved='Test2R' + ) + self.cst3 = Constituenta.objects.create( + alias='X3', + schema=self.rsform_owned.model, + order=2, + term_raw='Test3', + term_resolved='Test3', + definition_raw='Test1', + definition_resolved='Test2' + ) + self.invalid_cst = self.cst3.pk + 1337 + + @decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch') + def test_partial_update(self): + data = {'id': self.cst1.pk, 'convention': 'tt'} + self.executeForbidden(data=data, schema=self.rsform_unowned.model.pk) + + self.logout() + self.executeForbidden(data=data, schema=self.rsform_owned.model.pk) + + self.login() + response = self.executeOK(data=data, schema=self.rsform_owned.model.pk) + self.cst1.refresh_from_db() + self.assertEqual(response.data['convention'], 'tt') + self.assertEqual(self.cst1.convention, 'tt') + + self.executeOK(data=data, schema=self.rsform_owned.model.pk) + + + @decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch') + def test_update_resolved_no_refs(self): + data = { + 'id': self.cst3.pk, + 'term_raw': 'New term', + 'definition_raw': 'New def' + } + response = self.executeOK(data=data, schema=self.rsform_owned.model.pk) + self.cst3.refresh_from_db() + self.assertEqual(response.data['term_resolved'], 'New term') + self.assertEqual(self.cst3.term_resolved, 'New term') + self.assertEqual(response.data['definition_resolved'], 'New def') + self.assertEqual(self.cst3.definition_resolved, 'New def') + + + @decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch') + def test_update_resolved_refs(self): + data = { + 'id': self.cst3.pk, + 'term_raw': '@{X1|nomn,sing}', + 'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}' + } + response = self.executeOK(data=data, schema=self.rsform_owned.model.pk) + self.cst3.refresh_from_db() + self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) + self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved) + self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1') + self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1') + + + @decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch') + def test_readonly_cst_fields(self): + data = { + 'id': self.cst1.pk, + 'alias': 'X33', + 'order': 10 + } + response = self.executeOK(data=data, schema=self.rsform_owned.model.pk) + self.assertEqual(response.data['alias'], 'X1') + self.assertEqual(response.data['alias'], self.cst1.alias) + self.assertEqual(response.data['order'], self.cst1.order) + + +class TestInlineSynthesis(EndpointTester): + ''' Testing Operations endpoints. ''' + + + @decl_endpoint('/api/rsforms/inline-synthesis', method='patch') + def setUp(self): + super().setUp() + self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user) + self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user) + self.unowned = RSForm.create(title='Test3', alias='T3') + + + def test_inline_synthesis_inputs(self): + invalid_id = 1338 + data = { + 'receiver': self.unowned.model.pk, + 'source': self.schema1.model.pk, + 'items': [], + 'substitutions': [] + } + self.executeForbidden(data=data) + + data['receiver'] = invalid_id + self.executeBadData(data=data) + + data['receiver'] = self.schema1.model.pk + data['source'] = invalid_id + self.executeBadData(data=data) + + data['source'] = self.schema1.model.pk + self.executeOK(data=data) + + data['items'] = [invalid_id] + self.executeBadData(data=data) + + + def test_inline_synthesis(self): + ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete + ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2 + ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1 + ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1 + ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete + ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4 + ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2 + ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2 + ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items + + data = { + 'receiver': self.schema1.model.pk, + 'source': self.schema2.model.pk, + 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], + 'substitutions': [ + { + 'original': ks1_x1.pk, + 'substitution': ks2_s1.pk + }, + { + 'original': ks2_x1.pk, + 'substitution': ks1_s1.pk + } + ] + } + response = self.executeOK(data=data) + result = {item['alias']: item for item in response.data['items']} + self.assertEqual(len(result), 6) + self.assertEqual(result['X2']['order'], 1) + self.assertEqual(result['X4']['order'], 2) + self.assertEqual(result['S1']['definition_formal'], 'X2') + self.assertEqual(result['S2']['definition_formal'], 'X4×X4') + self.assertEqual(result['D1']['definition_formal'], r'S1\S2\X2') + self.assertEqual(result['D2']['definition_formal'], r'S2\S1\X4') diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py index 4cd7e04d..0c417454 100644 --- a/rsconcept/backend/apps/rsform/urls.py +++ b/rsconcept/backend/apps/rsform/urls.py @@ -9,11 +9,9 @@ library_router.register('rsforms', views.RSFormViewSet, 'RSForm') urlpatterns = [ - path('constituents/', views.ConstituentAPIView.as_view(), name='constituenta-detail'), path('rsforms/import-trs', views.TrsImportView.as_view()), path('rsforms/create-detailed', views.create_rsform), - - path('operations/inline-synthesis', views.inline_synthesis), + path('rsforms/inline-synthesis', views.inline_synthesis), path('rslang/parse-expression', views.parse_expression), path('rslang/to-ascii', views.convert_to_ascii), diff --git a/rsconcept/backend/apps/rsform/views/__init__.py b/rsconcept/backend/apps/rsform/views/__init__.py index df6a6b85..3daa0c4f 100644 --- a/rsconcept/backend/apps/rsform/views/__init__.py +++ b/rsconcept/backend/apps/rsform/views/__init__.py @@ -1,6 +1,4 @@ ''' REST API: Endpoint processors. ''' from .cctext import generate_lexeme, inflect, parse_text -from .constituents import ConstituentAPIView -from .operations import inline_synthesis -from .rsforms import RSFormViewSet, TrsImportView, create_rsform +from .rsforms import RSFormViewSet, TrsImportView, create_rsform, inline_synthesis from .rslang import convert_to_ascii, convert_to_math, parse_expression diff --git a/rsconcept/backend/apps/rsform/views/constituents.py b/rsconcept/backend/apps/rsform/views/constituents.py deleted file mode 100644 index 195a099c..00000000 --- a/rsconcept/backend/apps/rsform/views/constituents.py +++ /dev/null @@ -1,16 +0,0 @@ -''' Endpoints for Constituenta. ''' -from drf_spectacular.utils import extend_schema, extend_schema_view -from rest_framework import generics - -from shared import permissions - -from .. import models as m -from .. import serializers as s - - -@extend_schema(tags=['Constituenta']) -@extend_schema_view() -class ConstituentAPIView(generics.RetrieveUpdateAPIView, permissions.EditorMixin): - ''' Endpoint: Get / Update Constituenta. ''' - queryset = m.Constituenta.objects.all() - serializer_class = s.CstSerializer diff --git a/rsconcept/backend/apps/rsform/views/operations.py b/rsconcept/backend/apps/rsform/views/operations.py deleted file mode 100644 index 6fa68d8a..00000000 --- a/rsconcept/backend/apps/rsform/views/operations.py +++ /dev/null @@ -1,50 +0,0 @@ -''' Endpoints for RSForm. ''' -from typing import cast - -from django.db import transaction -from drf_spectacular.utils import extend_schema -from rest_framework import status as c -from rest_framework.decorators import api_view -from rest_framework.request import Request -from rest_framework.response import Response - -from .. import models as m -from .. import serializers as s - - -@extend_schema( - summary='Inline synthesis: merge one schema into another', - tags=['Operations'], - request=s.InlineSynthesisSerializer, - responses={c.HTTP_200_OK: s.RSFormParseSerializer} -) -@api_view(['PATCH']) -def inline_synthesis(request: Request): - ''' Endpoint: Inline synthesis. ''' - serializer = s.InlineSynthesisSerializer( - data=request.data, - context={'user': request.user} - ) - serializer.is_valid(raise_exception=True) - - receiver = m.RSForm(serializer.validated_data['receiver']) - items = cast(list[m.Constituenta], serializer.validated_data['items']) - - with transaction.atomic(): - new_items = receiver.insert_copy(items) - for substitution in serializer.validated_data['substitutions']: - original = cast(m.Constituenta, substitution['original']) - replacement = cast(m.Constituenta, substitution['substitution']) - if original in items: - index = next(i for (i, cst) in enumerate(items) if cst == original) - original = new_items[index] - else: - index = next(i for (i, cst) in enumerate(items) if cst == replacement) - replacement = new_items[index] - receiver.substitute(original, replacement) - receiver.restore_order() - - return Response( - status=c.HTTP_200_OK, - data=s.RSFormParseSerializer(receiver.model).data - ) diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 0666256a..85abd124 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -12,6 +12,7 @@ from rest_framework import views, viewsets from rest_framework.decorators import action, api_view from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import ValidationError from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead from apps.library.serializers import LibraryItemSerializer @@ -45,7 +46,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr 'substitute', 'restore_order', 'reset_aliases', - 'produce_structure' + 'produce_structure', + 'update_cst' ]: permission_list = [permissions.ItemEditor] elif self.action in [ @@ -88,15 +90,43 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr new_cst = m.RSForm(schema).create_cst(data, insert_after) schema.refresh_from_db() - response = Response( + return Response( status=c.HTTP_201_CREATED, data={ 'new_cst': s.CstSerializer(new_cst).data, 'schema': s.RSFormParseSerializer(schema).data } ) - response['Location'] = new_cst.get_absolute_url() - return response + + @extend_schema( + summary='update persistent attributes of a given constituenta', + tags=['RSForm'], + request=s.CstSerializer, + responses={ + c.HTTP_200_OK: s.CstSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='update-cst') + def update_cst(self, request: Request, pk): + ''' Update persistent attributes of a given constituenta. ''' + schema = self._get_item() + serializer = s.CstSerializer(data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + + cst = m.Constituenta.objects.get(pk=request.data['id']) + if cst.schema != schema: + raise ValidationError({ + f'schema': msg.constituentaNotInRSform(schema.title) + }) + serializer.update(instance=cst, validated_data=serializer.validated_data) + + return Response( + status=c.HTTP_200_OK, + data=s.CstSerializer(cst).data + ) @extend_schema( summary='produce the structure of a given constituenta', @@ -521,3 +551,41 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None]) data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC) data['location'] = request.data.get('location', LocationHead.USER) + + +@extend_schema( + summary='Inline synthesis: merge one schema into another', + tags=['Operations'], + request=s.InlineSynthesisSerializer, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} +) +@api_view(['PATCH']) +def inline_synthesis(request: Request): + ''' Endpoint: Inline synthesis. ''' + serializer = s.InlineSynthesisSerializer( + data=request.data, + context={'user': request.user} + ) + serializer.is_valid(raise_exception=True) + + receiver = m.RSForm(serializer.validated_data['receiver']) + items = cast(list[m.Constituenta], serializer.validated_data['items']) + + with transaction.atomic(): + new_items = receiver.insert_copy(items) + for substitution in serializer.validated_data['substitutions']: + original = cast(m.Constituenta, substitution['original']) + replacement = cast(m.Constituenta, substitution['substitution']) + if original in items: + index = next(i for (i, cst) in enumerate(items) if cst == original) + original = new_items[index] + else: + index = next(i for (i, cst) in enumerate(items) if cst == replacement) + replacement = new_items[index] + receiver.substitute(original, replacement) + receiver.restore_order() + + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(receiver.model).data + ) diff --git a/rsconcept/frontend/src/backend/constituents.ts b/rsconcept/frontend/src/backend/constituents.ts deleted file mode 100644 index 576e9e05..00000000 --- a/rsconcept/frontend/src/backend/constituents.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Endpoints: constituents. - */ - -import { IConstituentaMeta, ICstUpdateData } from '@/models/rsform'; - -import { AxiosPatch, FrontExchange } from './apiTransport'; - -export function patchConstituenta(target: string, request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/constituents/${target}`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/operations.ts b/rsconcept/frontend/src/backend/operations.ts deleted file mode 100644 index 5fce8876..00000000 --- a/rsconcept/frontend/src/backend/operations.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Endpoints: operations. - */ - -import { IInlineSynthesisData, IRSFormData } from '@/models/rsform'; - -import { AxiosPatch, FrontExchange } from './apiTransport'; - -export function patchInlineSynthesis(request: FrontExchange) { - AxiosPatch({ - endpoint: `/api/operations/inline-synthesis`, - request: request - }); -} diff --git a/rsconcept/frontend/src/backend/rsforms.ts b/rsconcept/frontend/src/backend/rsforms.ts index 51e198d6..c9f3f0eb 100644 --- a/rsconcept/frontend/src/backend/rsforms.ts +++ b/rsconcept/frontend/src/backend/rsforms.ts @@ -6,10 +6,13 @@ import { ILibraryCreateData, ILibraryItem } from '@/models/library'; import { ICstSubstituteData } from '@/models/oss'; import { IConstituentaList, + IConstituentaMeta, ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, + ICstUpdateData, + IInlineSynthesisData, IProduceStructureResponse, IRSFormData, IRSFormUploadData, @@ -68,6 +71,13 @@ export function postCreateConstituenta(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/${schema}/update-cst`, + request: request + }); +} + export function patchDeleteConstituenta(schema: string, request: FrontExchange) { AxiosPatch({ endpoint: `/api/rsforms/${schema}/delete-multiple-cst`, @@ -135,3 +145,10 @@ export function patchUploadTRS(target: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/rsforms/inline-synthesis`, + request: request + }); +} diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index c13f54a1..db985e23 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -3,7 +3,6 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { DataCallback } from '@/backend/apiTransport'; -import { patchConstituenta } from '@/backend/constituents'; import { deleteUnsubscribe, patchLibraryItem, @@ -14,16 +13,17 @@ import { postCreateVersion, postSubscribe } from '@/backend/library'; -import { patchInlineSynthesis } from '@/backend/operations'; import { getTRSFile, patchDeleteConstituenta, + patchInlineSynthesis, patchMoveConstituenta, patchProduceStructure, patchRenameConstituenta, patchResetAliases, patchRestoreOrder, patchSubstituteConstituents, + patchUpdateConstituenta, patchUploadTRS, postCreateConstituenta } from '@/backend/rsforms'; @@ -439,7 +439,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = const cstUpdate = useCallback( (data: ICstUpdateData, callback?: DataCallback) => { setProcessingError(undefined); - patchConstituenta(String(data.id), { + patchUpdateConstituenta(itemID, { data: data, showError: true, setLoading: setProcessing,