diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 2a221e89..754c432d 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -241,7 +241,7 @@ class RSForm(Model): def reset_aliases(self): ''' Recreate all aliases based on cst order. ''' mapping = self._create_reset_mapping() - self._apply_mapping(mapping) + self.apply_mapping(mapping, change_aliases=True) def _create_reset_mapping(self) -> dict[str, str]: bases = cast(dict[str, int], {}) @@ -257,11 +257,12 @@ class RSForm(Model): return mapping @transaction.atomic - def _apply_mapping(self, mapping: dict[str, str]): + def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False): + ''' Apply rename mapping. ''' cst_list = self.constituents().order_by('order') for cst in cst_list: modified = False - if cst.alias in mapping: + if change_aliases and cst.alias in mapping: modified = True cst.alias = mapping[cst.alias] expression = apply_mapping_pattern(cst.definition_formal, mapping, _GLOBAL_ID_PATTERN) diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index abfce0fb..9fc232be 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -1,5 +1,5 @@ ''' Serializers for conceptual schema API. ''' -from typing import Optional +from typing import Optional, cast from rest_framework import serializers from django.db import transaction @@ -264,6 +264,30 @@ class CstCreateSerializer(serializers.ModelSerializer): fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after' +class CstRenameSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta renaming. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = 'id', 'alias', 'cst_type' + + def validate(self, attrs): + schema = cast(RSForm, self.context['schema']) + old_cst = Constituenta.objects.get(pk=self.initial_data['id']) + if old_cst.schema != schema: + raise serializers.ValidationError({ + 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.title}' + }) + if old_cst.alias == self.initial_data['alias']: + raise serializers.ValidationError({ + 'alias': f'Имя конституенты должно отличаться от текущего: {self.initial_data["alias"]}' + }) + self.instance = old_cst + attrs['schema'] = schema + attrs['id'] = self.initial_data['id'] + return attrs + + class CstListSerlializer(serializers.Serializer): ''' Serializer: List of constituents from one origin. ''' items = serializers.ListField( diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 356ac659..223fddcf 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -246,6 +246,56 @@ class TestRSFormViewset(APITestCase): x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x4.order, 3) + def test_rename_constituenta(self): + self.cst1 = Constituenta.objects.create( + alias='X1', schema=self.rsform_owned, order=1, convention='Test', + term_raw='Test1', term_resolved='Test1', + term_forms=[{'text':'form1', 'tags':'sing,datv'}]) + self.cst2 = Constituenta.objects.create( + alias='X2', schema=self.rsform_unowned, order=1, convention='Test1', + term_raw='Test2', term_resolved='Test2') + self.cst3 = Constituenta.objects.create( + alias='X3', schema=self.rsform_owned, order=2, + term_raw='Test3', term_resolved='Test3', + definition_raw='Test1', definition_resolved='Test2') + + data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk}) + response = self.client.patch(f'/api/rsforms/{self.rsform_unowned.id}/cst-rename/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 403) + + response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/cst-rename/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 400) + + data = json.dumps({'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk}) + response = self.client.patch(f'/api/rsforms/{self.rsform_owned.id}/cst-rename/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 400) + + data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk}) + schema = self.rsform_owned + d1 = Constituenta.objects.create(schema=schema, alias='D1', cst_type='term', order=4) + d1.term_raw = '@{X1|plur}' + d1.definition_formal = 'X1' + d1.save() + + self.assertEqual(self.cst1.order, 1) + self.assertEqual(self.cst1.alias, 'X1') + self.assertEqual(self.cst1.cst_type, CstType.BASE) + response = self.client.patch(f'/api/rsforms/{schema.id}/cst-rename/', + data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['new_cst']['alias'], 'D2') + self.assertEqual(response.data['new_cst']['cst_type'], 'term') + d1.refresh_from_db() + self.cst1.refresh_from_db() + self.assertEqual(d1.term_resolved, '') + self.assertEqual(d1.term_raw, '@{D2|plur}') + self.assertEqual(self.cst1.order, 2) + self.assertEqual(self.cst1.alias, 'D2') + self.assertEqual(self.cst1.cst_type, CstType.TERM) + def test_create_constituenta_data(self): data = json.dumps({ 'alias': 'X3', diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index c8b23a42..ff43276b 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -1,5 +1,6 @@ ''' REST API: RSForms for conceptual schemas. ''' import json +from django.db import transaction from django.http import HttpResponse from django_filters.rest_framework import DjangoFilterBackend from django.db.models import Q @@ -63,7 +64,7 @@ class RSFormViewSet(viewsets.ModelViewSet): def get_permissions(self): if self.action in ['update', 'destroy', 'partial_update', 'load_trs', - 'cst_create', 'cst_multidelete', 'reset_aliases']: + 'cst_create', 'cst_multidelete', 'reset_aliases', 'cst_rename']: permission_classes = [utils.ObjectOwnerOrAdmin] elif self.action in ['create', 'claim', 'clone']: permission_classes = [permissions.IsAuthenticated] @@ -87,6 +88,25 @@ class RSFormViewSet(viewsets.ModelViewSet): response['Location'] = new_cst.get_absolute_url() return response + @transaction.atomic + @action(detail=True, methods=['patch'], url_path='cst-rename') + def cst_rename(self, request, pk): + ''' Rename constituenta possibly changing type. ''' + schema = self._get_schema() + serializer = serializers.CstRenameSerializer(data=request.data, context={'schema': schema}) + serializer.is_valid(raise_exception=True) + old_alias = models.Constituenta.objects.get(pk=request.data['id']).alias + serializer.save() + mapping = { old_alias: serializer.validated_data['alias'] } + schema.apply_mapping(mapping, change_aliases=False) + schema.update_order() + schema.refresh_from_db() + cst = models.Constituenta.objects.get(pk=serializer.validated_data['id']) + return Response(status=200, data={ + 'new_cst': serializers.ConstituentaSerializer(cst).data, + 'schema': models.PyConceptAdapter(schema).full() + }) + @action(detail=True, methods=['patch'], url_path='cst-multidelete') def cst_multidelete(self, request, pk): ''' Endpoint: Delete multiple constituents. ''' diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index 2cbcf24c..74f6e145 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -227,10 +227,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => { setLoading: setProcessing, onError: error => setError(error), onSuccess: newData => { - reload(setProcessing, () => { if (callback) callback(newData); }) + setSchema(newData.schema); + if (callback) callback(newData.new_cst); } }); - }, [setError, reload, schemaID]); + }, [setError, setSchema, schemaID]); const cstMoveTo = useCallback( (data: ICstMovetoData, callback?: () => void) => { diff --git a/rsconcept/frontend/src/utils/backendAPI.ts b/rsconcept/frontend/src/utils/backendAPI.ts index 14f391cf..92197a1e 100644 --- a/rsconcept/frontend/src/utils/backendAPI.ts +++ b/rsconcept/frontend/src/utils/backendAPI.ts @@ -210,7 +210,7 @@ export function patchConstituenta(target: string, request: FrontExchange) { +export function patchRenameConstituenta(schema: string, request: FrontExchange) { AxiosPatch({ title: `Renaming constituenta id=${request.data.id} for schema id=${schema}`, endpoint: `/api/rsforms/${schema}/cst-rename/`,