diff --git a/rsconcept/backend/apps/rsform/graph.py b/rsconcept/backend/apps/rsform/graph.py index 137b9bb2..528220c0 100644 --- a/rsconcept/backend/apps/rsform/graph.py +++ b/rsconcept/backend/apps/rsform/graph.py @@ -1,12 +1,12 @@ ''' Utility: Graph implementation. ''' -from typing import Dict, Iterable, Optional, cast +from typing import Iterable, Optional, cast class Graph: ''' Directed graph. ''' - def __init__(self, graph: Optional[Dict[str, list[str]]]=None): + def __init__(self, graph: Optional[dict[str, list[str]]]=None): if graph is None: - self._graph = cast(Dict[str, list[str]], {}) + self._graph = cast(dict[str, list[str]], {}) else: self._graph = graph diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/apps/rsform/messages.py index ba1f9cbb..d65d2b9a 100644 --- a/rsconcept/backend/apps/rsform/messages.py +++ b/rsconcept/backend/apps/rsform/messages.py @@ -16,6 +16,9 @@ def renameTrivial(name: str): def substituteTrivial(name: str): return f'Отождествление конституенты с собой не корректно: {name}' +def substituteDouble(name: str): + return f'Повторное отождествление: {name}' + def aliasTaken(name: str): return f'Имя уже используется: {name}' diff --git a/rsconcept/backend/apps/rsform/models/api_RSForm.py b/rsconcept/backend/apps/rsform/models/api_RSForm.py index 318be0c4..a78c7513 100644 --- a/rsconcept/backend/apps/rsform/models/api_RSForm.py +++ b/rsconcept/backend/apps/rsform/models/api_RSForm.py @@ -1,5 +1,5 @@ ''' Models: RSForm API. ''' -from typing import Dict, Iterable, Optional, Union, cast +from typing import Iterable, Optional, Union, cast from django.db import transaction from django.db.models import QuerySet @@ -53,6 +53,7 @@ class RSForm: ''' Trigger cascade resolutions when term changes. ''' graph_terms = self._term_graph() expansion = graph_terms.expand_outputs(changed) + expanded_change = list(changed) + expansion resolver = self.resolver() if len(expansion) > 0: for alias in graph_terms.topological_order(): @@ -67,7 +68,7 @@ class RSForm: resolver.context[cst.alias] = Entity(cst.alias, resolved) graph_defs = self._definition_graph() - update_defs = set(expansion + graph_defs.expand_outputs(expansion + changed)).union(changed) + update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed) if len(update_defs) == 0: return for alias in update_defs: @@ -126,11 +127,11 @@ class RSForm: position = self._get_insert_position(position) self._shift_positions(position, count) - indices: Dict[str, int] = {} + indices: dict[str, int] = {} for (value, _) in CstType.choices: indices[value] = self.get_max_index(cast(CstType, value)) - mapping: Dict[str, str] = {} + mapping: dict[str, str] = {} for cst in items: indices[cst.cst_type] = indices[cst.cst_type] + 1 newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 307b5d7c..e7b3aa81 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -194,34 +194,6 @@ class RSFormParseSerializer(serializers.ModelSerializer): return data -class CstSubstituteSerializerBase(serializers.Serializer): - ''' Serializer: Basic substitution. ''' - original = PKField(many=False, queryset=Constituenta.objects.all()) - substitution = PKField(many=False, queryset=Constituenta.objects.all()) - transfer_term = serializers.BooleanField(required=False, default=False) - - -class CstSubstituteSerializer(CstSubstituteSerializerBase): - ''' Serializer: Constituenta substitution. ''' - def validate(self, attrs): - schema = cast(LibraryItem, self.context['schema']) - original_cst = cast(Constituenta, attrs['original']) - substitution_cst = cast(Constituenta, attrs['substitution']) - if original_cst.alias == substitution_cst.alias: - raise serializers.ValidationError({ - 'alias': msg.substituteTrivial(original_cst.alias) - }) - if original_cst.schema != schema: - raise serializers.ValidationError({ - 'original': msg.constituentaNotOwned(schema.title) - }) - if substitution_cst.schema != schema: - raise serializers.ValidationError({ - 'substitution': msg.constituentaNotOwned(schema.title) - }) - return attrs - - class CstTargetSerializer(serializers.Serializer): ''' Serializer: Target single Constituenta. ''' target = PKField(many=False, queryset=Constituenta.objects.all()) @@ -289,6 +261,46 @@ class CstMoveSerializer(CstListSerializer): move_to = serializers.IntegerField() +class CstSubstituteSerializerBase(serializers.Serializer): + ''' Serializer: Basic substitution. ''' + original = PKField(many=False, queryset=Constituenta.objects.all()) + substitution = PKField(many=False, queryset=Constituenta.objects.all()) + transfer_term = serializers.BooleanField(required=False, default=False) + + +class CstSubstituteSerializer(serializers.Serializer): + ''' Serializer: Constituenta substitution. ''' + substitutions = serializers.ListField( + child=CstSubstituteSerializerBase(), + min_length=1 + ) + + def validate(self, attrs): + schema = cast(LibraryItem, self.context['schema']) + deleted = set() + for item in attrs['substitutions']: + original_cst = cast(Constituenta, item['original']) + substitution_cst = cast(Constituenta, item['substitution']) + if original_cst.pk in deleted: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substituteDouble(original_cst.alias) + }) + if original_cst.alias == substitution_cst.alias: + raise serializers.ValidationError({ + 'alias': msg.substituteTrivial(original_cst.alias) + }) + if original_cst.schema != schema: + raise serializers.ValidationError({ + 'original': msg.constituentaNotOwned(schema.title) + }) + if substitution_cst.schema != schema: + raise serializers.ValidationError({ + 'substitution': msg.constituentaNotOwned(schema.title) + }) + deleted.add(original_cst.pk) + return attrs + + class InlineSynthesisSerializer(serializers.Serializer): ''' Serializer: Inline synthesis operation input. ''' receiver = PKField(many=False, queryset=LibraryItem.objects.all()) @@ -313,6 +325,7 @@ class InlineSynthesisSerializer(serializers.Serializer): raise serializers.ValidationError({ f'{cst.id}': msg.constituentaNotOwned(schema_in.title) }) + deleted = set() for item in attrs['substitutions']: original_cst = cast(Constituenta, item['original']) substitution_cst = cast(Constituenta, item['substitution']) @@ -334,4 +347,9 @@ class InlineSynthesisSerializer(serializers.Serializer): raise serializers.ValidationError({ f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title) }) + if original_cst.pk in deleted: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substituteDouble(original_cst.alias) + }) + deleted.add(original_cst.pk) return attrs 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 1adeed6f..2e8b2c23 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -260,7 +260,7 @@ class TestRSFormViewset(EndpointTester): @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') - def test_substitute_constituenta(self): + def test_substitute_single(self): x1 = self.schema.insert_new( alias='X1', term_raw='Test1', @@ -273,14 +273,14 @@ class TestRSFormViewset(EndpointTester): ) unowned = self.unowned.insert_new('X2') - data = {'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True} + data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]} self.assertForbidden(data, item=self.unowned_id) self.assertBadData(data, item=self.schema_id) - data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True} + data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]} self.assertBadData(data, item=self.schema_id) - data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True} + data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]} self.assertBadData(data, item=self.schema_id) d1 = self.schema.insert_new( @@ -288,7 +288,7 @@ class TestRSFormViewset(EndpointTester): term_raw='@{X2|sing,datv}', definition_formal='X1' ) - data = {'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True} + data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]} response = self.execute(data, item=self.schema_id) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -298,6 +298,53 @@ class TestRSFormViewset(EndpointTester): self.assertEqual(d1.term_resolved, 'form1') self.assertEqual(d1.definition_formal, 'X2') + @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( + alias='D3', + definition_formal='X1 \ X2' + ) + + data = {'substitutions': []} + self.assertBadData(data) + + data = {'substitutions': [ + { + 'original': x1.pk, + 'substitution': d1.pk, + 'transfer_term': True + }, + { + 'original': x1.pk, + 'substitution': d2.pk, + 'transfer_term': True + } + ]} + self.assertBadData(data) + + data = {'substitutions': [ + { + 'original': x1.pk, + 'substitution': d1.pk, + 'transfer_term': True + }, + { + 'original': x2.pk, + 'substitution': d2.pk, + 'transfer_term': True + } + ]} + response = self.execute(data, item=self.schema_id) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + d3.refresh_from_db() + self.assertEqual(d3.definition_formal, 'D1 \ D2') + @decl_endpoint('/api/rsforms/{item}/cst-create', method='post') def test_create_constituenta_data(self): diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index b3f60d4d..f801e3d2 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -147,11 +147,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr context={'schema': schema.item} ) serializer.is_valid(raise_exception=True) - schema.substitute( - original=serializer.validated_data['original'], - substitution=serializer.validated_data['substitution'], - transfer_term=serializer.validated_data['transfer_term'] - ) + for substitution in serializer.validated_data['substitutions']: + original = cast(m.Constituenta, substitution['original']) + replacement = cast(m.Constituenta, substitution['substitution']) + schema.substitute(original, replacement, substitution['transfer_term']) schema.item.refresh_from_db() return Response( status=c.HTTP_200_OK, diff --git a/rsconcept/backend/cctext/context.py b/rsconcept/backend/cctext/context.py index a184055d..de487cd8 100644 --- a/rsconcept/backend/cctext/context.py +++ b/rsconcept/backend/cctext/context.py @@ -1,5 +1,5 @@ ''' Term context for reference resolution. ''' -from typing import Iterable, Dict, Optional, TypedDict +from typing import Iterable, Optional, TypedDict from .ruparser import PhraseParser from .rumodel import WordTag @@ -81,4 +81,4 @@ class Entity: # Represents term context for resolving entity references. -TermContext = Dict[str, Entity] +TermContext = dict[str, Entity] diff --git a/rsconcept/frontend/src/components/man/HelpRSTemplates.tsx b/rsconcept/frontend/src/components/man/HelpRSTemplates.tsx index baae2107..0f3bb697 100644 --- a/rsconcept/frontend/src/components/man/HelpRSTemplates.tsx +++ b/rsconcept/frontend/src/components/man/HelpRSTemplates.tsx @@ -2,7 +2,7 @@ function HelpRSTemplates() { // prettier-ignore return (
Портал предоставляет быстрый доступ к часто используемым выражениям с помощью функции создания конституенты из шаблона
Источником шаблонов является Банк выражений, содержащий параметризованные понятия и утверждения, сгруппированные по разделам
Сначала выбирается шаблон выражения (вкладка Шаблон)
diff --git a/rsconcept/frontend/src/components/select/ConstituentaMultiPicker.tsx b/rsconcept/frontend/src/components/select/ConstituentaMultiPicker.tsx index 343b3e34..99d7d7ff 100644 --- a/rsconcept/frontend/src/components/select/ConstituentaMultiPicker.tsx +++ b/rsconcept/frontend/src/components/select/ConstituentaMultiPicker.tsx @@ -100,15 +100,15 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele