From 1aa80c3dce867a9903c60a83b797a300ca0cb673 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 15 Jan 2024 23:37:14 +0300 Subject: [PATCH] Add endpoint for constituenta substitution --- .vscode/settings.json | 3 + rsconcept/backend/apps/rsform/messages.py | 29 ++++ rsconcept/backend/apps/rsform/models.py | 50 +++++-- rsconcept/backend/apps/rsform/serializers.py | 61 ++++++-- .../backend/apps/rsform/tests/t_models.py | 41 +++++- .../backend/apps/rsform/tests/t_views.py | 138 +++++++++++++++--- rsconcept/backend/apps/rsform/views.py | 28 +++- rsconcept/backend/apps/users/messages.py | 8 + rsconcept/backend/apps/users/serializers.py | 23 +-- rsconcept/backend/cctext/conceptapi.py | 6 +- rsconcept/backend/cctext/reference.py | 6 +- rsconcept/backend/cctext/resolver.py | 18 +-- rsconcept/backend/cctext/ruparser.py | 8 +- rsconcept/backend/cctext/tests/t_resolver.py | 16 +- 14 files changed, 339 insertions(+), 96 deletions(-) create mode 100644 rsconcept/backend/apps/rsform/messages.py create mode 100644 rsconcept/backend/apps/users/messages.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 8ef7cfe8..d08d75cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -47,6 +47,7 @@ "clsx", "codemirror", "Constituenta", + "corsheaders", "csrftoken", "cstlist", "csttype", @@ -67,6 +68,7 @@ "GRND", "impr", "inan", + "incapsulation", "indc", "INFN", "Infr", @@ -84,6 +86,7 @@ "NUMR", "Opencorpora", "perfectivity", + "PNCT", "ponomarev", "PRCL", "PRTF", diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/apps/rsform/messages.py new file mode 100644 index 00000000..a4036347 --- /dev/null +++ b/rsconcept/backend/apps/rsform/messages.py @@ -0,0 +1,29 @@ +''' Utility: Text messages. ''' +# pylint: skip-file + +def constituentaNotOwned(title: str): + return f'Конституента не принадлежит схеме: {title}' + +def constituentaNotExists(): + return 'Конституента не существует' + +def renameTrivial(name: str): + return f'Имя должно отличаться от текущего: {name}' + +def substituteTrivial(name: str): + return f'Отождествление конституенты с собой не корректно: {name}' + +def renameTaken(name: str): + return f'Имя уже используется: {name}' + +def pyconceptFailure(): + return 'Invalid data response from pyconcept' + +def libraryTypeUnexpected(): + return 'Attempting to use invalid adaptor for non-RSForm item' + +def exteorFileVersionNotSupported(): + return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' + +def positionNegative(): + return 'Invalid position: should be positive integer' diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 06126ebc..b8eb181a 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -15,10 +15,11 @@ from apps.users.models import User from cctext import Resolver, Entity, extract_entities, split_grams, TermForm from .graph import Graph from .utils import apply_pattern +from . import messages as msg _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}') -_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') +_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line class LibraryItemType(TextChoices): @@ -125,7 +126,7 @@ class LibraryItem(Model): def subscribers(self) -> list[User]: ''' Get all subscribers for this item . ''' - return [s.user for s in Subscription.objects.filter(item=self.pk)] + return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)] @transaction.atomic def save(self, *args, **kwargs): @@ -272,7 +273,7 @@ class RSForm: ''' RSForm is a math form of capturing conceptual schema. ''' def __init__(self, item: LibraryItem): if item.item_type != LibraryItemType.RSFORM: - raise ValueError('Attempting to use invalid adaptor for non-RSForm item') + raise ValueError(msg.libraryTypeUnexpected()) self.item = item @staticmethod @@ -330,11 +331,12 @@ class RSForm: @transaction.atomic def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta': - ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' + ''' Insert new constituenta at given position. + All following constituents order is shifted by 1 position. ''' if position <= 0: - raise ValidationError('Invalid position: should be positive integer') + raise ValidationError(msg.positionNegative()) if self.constituents().filter(alias=alias).exists(): - raise ValidationError(f'Alias taken {alias}') + raise ValidationError(msg.renameTaken(alias)) currentSize = self.constituents().count() position = max(1, min(position, currentSize + 1)) update_list = \ @@ -357,9 +359,9 @@ class RSForm: @transaction.atomic def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': - ''' Insert new constituenta at last position ''' + ''' Insert new constituenta at last position. ''' if self.constituents().filter(alias=alias).exists(): - raise ValidationError(f'Alias taken {alias}') + raise ValidationError(msg.renameTaken(alias)) position = 1 if self.constituents().exists(): position += self.constituents().count() @@ -398,7 +400,7 @@ class RSForm: @transaction.atomic def delete_cst(self, listCst): - ''' Delete multiple constituents. Do not check if listCst are from this schema ''' + ''' Delete multiple constituents. Do not check if listCst are from this schema. ''' for cst in listCst: cst.delete() self._reset_order() @@ -426,8 +428,26 @@ class RSForm: cst.refresh_from_db() return cst + @transaction.atomic + def substitute( + self, + original: 'Constituenta', + substitution: 'Constituenta', + transfer_term: bool + ): + ''' Execute constituenta substitution. ''' + assert original.pk != substitution.pk + mapping = { original.alias: substitution.alias } + self.apply_mapping(mapping) + if transfer_term: + substitution.term_raw = original.term_raw + substitution.term_forms = original.term_forms + substitution.save() + original.delete() + self.on_term_change([substitution.alias]) + def reset_aliases(self): - ''' Recreate all aliases based on cst order. ''' + ''' Recreate all aliases based on constituents order. ''' mapping = self._create_reset_mapping() self.apply_mapping(mapping, change_aliases=True) @@ -508,7 +528,10 @@ class RSForm: def _term_graph(self) -> Graph: result = Graph() - cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order') + cst_list = \ + self.constituents() \ + .only('order', 'alias', 'term_raw') \ + .order_by('order') for cst in cst_list: result.add_node(cst.alias) for cst in cst_list: @@ -519,7 +542,10 @@ class RSForm: def _definition_graph(self) -> Graph: result = Graph() - cst_list = self.constituents().only('order', 'alias', 'definition_raw').order_by('order') + cst_list = \ + self.constituents() \ + .only('order', 'alias', 'definition_raw') \ + .order_by('order') for cst in cst_list: result.add_node(cst.alias) for cst in cst_list: diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index e4eafd3c..6e2083cc 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -9,6 +9,7 @@ from cctext import Resolver, Reference, ReferenceType, EntityReference, Syntacti from .utils import fix_old_references from .models import Constituenta, LibraryItem, RSForm +from . import messages as msg _CST_TYPE = 'constituenta' _TRS_TYPE = 'rsform' @@ -16,6 +17,8 @@ _TRS_VERSION_MIN = 16 _TRS_VERSION = 16 _TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' +ConstituentaID = serializers.IntegerField +NodeID = serializers.IntegerField class FileSerializer(serializers.Serializer): ''' Serializer: File input. ''' @@ -99,7 +102,7 @@ class NodeDataSerializer(serializers.Serializer): class ASTNodeSerializer(serializers.Serializer): ''' Serializer: Syntax tree node. ''' - uid = serializers.IntegerField() + uid = NodeID() parent = serializers.IntegerField() # type: ignore typeID = serializers.IntegerField() start = serializers.IntegerField() @@ -148,7 +151,7 @@ class ConstituentaSerializer(serializers.ModelSerializer): read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') def update(self, instance: Constituenta, validated_data) -> Constituenta: - data = validated_data # Note: create alias for better code readability + data = validated_data # Note: use alias for better code readability schema = RSForm(instance.schema) definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None term: Optional[str] = data['term_raw'] if 'term_raw' in data else None @@ -175,7 +178,10 @@ class CstCreateSerializer(serializers.ModelSerializer): class Meta: ''' serializer metadata. ''' model = Constituenta - fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after', 'term_forms' + fields = \ + 'alias', 'cst_type', 'convention', \ + 'term_raw', 'definition_raw', 'definition_formal', \ + 'insert_after', 'term_forms' class CstRenameSerializer(serializers.ModelSerializer): @@ -191,15 +197,15 @@ class CstRenameSerializer(serializers.ModelSerializer): new_alias = self.initial_data['alias'] if old_cst.schema != schema.item: raise serializers.ValidationError({ - 'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}' + 'id': msg.constituentaNotOwned(schema.item.title) }) if old_cst.alias == new_alias: raise serializers.ValidationError({ - 'alias': f'Имя конституенты должно отличаться от текущего: {new_alias}' + 'alias': msg.renameTrivial(new_alias) }) if schema.constituents().filter(alias=new_alias).exists(): raise serializers.ValidationError({ - 'alias': f'Конституента с таким именем уже существует: {new_alias}' + 'alias': msg.renameTaken(new_alias) }) self.instance = old_cst attrs['schema'] = schema.item @@ -207,6 +213,34 @@ class CstRenameSerializer(serializers.ModelSerializer): return attrs +class CstSubstituteSerializer(serializers.Serializer): + ''' Serializer: Constituenta substitution. ''' + original = ConstituentaID() + substitution = ConstituentaID() + transfer_term = serializers.BooleanField(required=False, default=False) + + def validate(self, attrs): + schema = cast(RSForm, self.context['schema']) + original_cst = Constituenta.objects.get(pk=self.initial_data['original']) + substitution_cst = Constituenta.objects.get(pk=self.initial_data['substitution']) + if original_cst.alias == substitution_cst.alias: + raise serializers.ValidationError({ + 'alias': msg.substituteTrivial(original_cst.alias) + }) + if original_cst.schema != schema.item: + raise serializers.ValidationError({ + 'original': msg.constituentaNotOwned(schema.item.title) + }) + if substitution_cst.schema != schema.item: + raise serializers.ValidationError({ + 'substitution': msg.constituentaNotOwned(schema.item.title) + }) + attrs['original'] = original_cst + attrs['substitution'] = substitution_cst + attrs['transfer_term'] = self.initial_data['transfer_term'] + return attrs + + class CstListSerializer(serializers.Serializer): ''' Serializer: List of constituents from one origin. ''' items = serializers.ListField( @@ -220,12 +254,13 @@ class CstListSerializer(serializers.Serializer): try: cst = Constituenta.objects.get(pk=item) except Constituenta.DoesNotExist as exception: - raise serializers.ValidationError( - {f"{item}": 'Конституента не существует'} - ) from exception + raise serializers.ValidationError({ + f'{item}': msg.constituentaNotExists + }) from exception if cst.schema != schema.item: - raise serializers.ValidationError( - {'items': f'Конституенты должны относиться к данной схеме: {item}'}) + raise serializers.ValidationError({ + f'{item}': msg.constituentaNotOwned(schema.item.title) + }) cstList.append(cst) attrs['constituents'] = cstList return attrs @@ -310,7 +345,7 @@ class PyConceptAdapter: Warning! Does not include texts. ''' self._produce_response() if self._checked_data is None: - raise ValueError('Invalid data response from pyconcept') + raise ValueError(msg.pyconceptFailure()) return self._checked_data def _prepare_request(self) -> dict: @@ -481,7 +516,7 @@ class RSFormTRSSerializer(serializers.Serializer): or self.initial_data['version'] < _TRS_VERSION_MIN \ or self.initial_data['version'] > _TRS_VERSION: raise serializers.ValidationError({ - 'version': 'Некорректная версия файла Экстеор. Сохраните файл в новой версии' + 'version': msg.exteorFileVersionNotSupported() }) return attrs diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index 838d673c..e77218f1 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -242,19 +242,44 @@ class TestRSForm(TestCase): self.assertEqual(cst2.term_resolved, 'слон') self.assertEqual(cst2.definition_resolved, 'слонам слоны') - def test_delete_cst(self): + def test_apply_mapping(self): + schema = RSForm.create(title='Test') + x1 = schema.insert_last('X1', CstType.BASE) + x2 = schema.insert_last('X11', CstType.BASE) + d1 = schema.insert_last('D1', CstType.TERM) + d1.definition_formal = 'X1 = X11 = X2' + d1.definition_raw = '@{X11|sing}' + d1.convention = 'X1' + d1.term_raw = '@{X1|plur}' + d1.save() + + schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'}) + d1.refresh_from_db() + self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression') + self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition') + self.assertEqual(d1.convention, 'X3', msg='Map IDs in convention') + self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term') + self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping') + self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping') + + def test_substitute(self): schema = RSForm.create(title='Test') x1 = schema.insert_last('X1', CstType.BASE) x2 = schema.insert_last('X2', CstType.BASE) d1 = schema.insert_last('D1', CstType.TERM) - d2 = schema.insert_last('D2', CstType.TERM) - schema.delete_cst([x2, d1]) - x1.refresh_from_db() - d2.refresh_from_db() - schema.item.refresh_from_db() + d1.definition_formal = x1.alias + d1.save() + x1.term_raw = 'Test' + x1.save() + x2.term_raw = 'Test2' + x2.save() + + schema.substitute(x1, x2, True) + x2.refresh_from_db() + d1.refresh_from_db() self.assertEqual(schema.constituents().count(), 2) - self.assertEqual(x1.order, 1) - self.assertEqual(d2.order, 2) + self.assertEqual(x2.term_raw, 'Test') + self.assertEqual(d1.definition_formal, x2.alias) def test_move_cst(self): schema = RSForm.create(title='Test') diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index f9640b09..1e7ed831 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -36,16 +36,30 @@ class TestConstituentaAPI(APITestCase): 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', schema=self.rsform_owned.item, order=1, convention='Test', - term_raw='Test1', term_resolved='Test1R', + alias='X1', + schema=self.rsform_owned.item, + order=1, + convention='Test', + term_raw='Test1', + term_resolved='Test1R', term_forms=[{'text':'form1', 'tags':'sing,datv'}]) self.cst2 = Constituenta.objects.create( - alias='X2', schema=self.rsform_unowned.item, order=1, convention='Test1', - term_raw='Test2', term_resolved='Test2R') + alias='X2', + schema=self.rsform_unowned.item, + order=1, + convention='Test1', + term_raw='Test2', + term_resolved='Test2R' + ) self.cst3 = Constituenta.objects.create( - alias='X3', schema=self.rsform_owned.item, order=2, - term_raw='Test3', term_resolved='Test3', - definition_raw='Test1', definition_resolved='Test2') + alias='X3', + schema=self.rsform_owned.item, + order=2, + term_raw='Test3', + term_resolved='Test3', + definition_raw='Test1', + definition_resolved='Test2' + ) def test_retrieve(self): response = self.client.get(f'/api/constituents/{self.cst1.id}') @@ -421,8 +435,18 @@ class TestRSFormViewset(APITestCase): self.assertEqual(response.status_code, 403) item = self.owned.item - Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1) - x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2) + Constituenta.objects.create( + schema=item, + alias='X1', + cst_type='basic', + order=1 + ) + x2 = Constituenta.objects.create( + schema=item, + alias='X2', + cst_type='basic', + order=2 + ) response = self.client.post( f'/api/rsforms/{item.id}/cst-create', data=data, format='json' @@ -452,21 +476,29 @@ class TestRSFormViewset(APITestCase): def test_rename_constituenta(self): cst1 = Constituenta.objects.create( - alias='X1', schema=self.owned.item, order=1, convention='Test', - term_raw='Test1', term_resolved='Test1', + alias='X1', + schema=self.owned.item, + order=1, + convention='Test', + term_raw='Test1', + term_resolved='Test1', term_forms=[{'text':'form1', 'tags':'sing,datv'}] ) cst2 = Constituenta.objects.create( - alias='X2', schema=self.unowned.item, order=1, convention='Test1', - term_raw='Test2', term_resolved='Test2' + alias='X2', + schema=self.unowned.item, + order=1 ) cst3 = Constituenta.objects.create( - alias='X3', schema=self.owned.item, order=2, - term_raw='Test3', term_resolved='Test3', - definition_raw='Test1', definition_resolved='Test2' + alias='X3', + schema=self.owned.item, order=2, + term_raw='Test3', + term_resolved='Test3', + definition_raw='Test1', + definition_resolved='Test2' ) - data = {'alias': 'D2', 'cst_type': 'term', 'id': cst2.pk} + data = {'id': cst2.pk, 'alias': 'D2', 'cst_type': 'term'} response = self.client.patch( f'/api/rsforms/{self.unowned.item.id}/cst-rename', data=data, format='json' @@ -479,14 +511,14 @@ class TestRSFormViewset(APITestCase): ) self.assertEqual(response.status_code, 400) - data = {'alias': cst1.alias, 'cst_type': 'term', 'id': cst1.pk} + data = {'id': cst1.pk, 'alias': cst1.alias, 'cst_type': 'term'} response = self.client.patch( f'/api/rsforms/{self.owned.item.id}/cst-rename', data=data, format='json' ) self.assertEqual(response.status_code, 400) - data = {'alias': cst3.alias, 'id': cst1.pk} + data = {'id': cst1.pk, 'alias': cst3.alias} response = self.client.patch( f'/api/rsforms/{self.owned.item.id}/cst-rename', data=data, format='json' @@ -520,6 +552,74 @@ class TestRSFormViewset(APITestCase): self.assertEqual(cst1.alias, 'D2') self.assertEqual(cst1.cst_type, CstType.TERM) + def test_substitute_constituenta(self): + x1 = Constituenta.objects.create( + alias='X1', + schema=self.owned.item, + order=1, + term_raw='Test1', + term_resolved='Test1', + term_forms=[{'text':'form1', 'tags':'sing,datv'}] + ) + x2 = Constituenta.objects.create( + alias='X2', + schema=self.owned.item, + order=2, + term_raw='Test2' + ) + unowned = Constituenta.objects.create( + alias='X2', + schema=self.unowned.item, + order=1 + ) + + data = {'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True} + response = self.client.patch( + f'/api/rsforms/{self.unowned.item.id}/cst-substitute', + data=data, format='json' + ) + self.assertEqual(response.status_code, 403) + + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-substitute', + data=data, format='json' + ) + self.assertEqual(response.status_code, 400) + + data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True} + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-substitute', + data=data, format='json' + ) + self.assertEqual(response.status_code, 400) + + data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True} + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-substitute', + data=data, format='json' + ) + self.assertEqual(response.status_code, 400) + + d1 = Constituenta.objects.create( + alias='D1', + schema=self.owned.item, + order=3, + term_raw='@{X2|sing,datv}', + definition_formal='X1' + ) + data = {'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True} + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-substitute', + data=data, format='json' + ) + self.assertEqual(response.status_code, 200) + + d1.refresh_from_db() + x2.refresh_from_db() + self.assertEqual(x2.term_raw, 'Test1') + self.assertEqual(d1.term_resolved, 'form1') + self.assertEqual(d1.definition_formal, 'X2') + def test_create_constituenta_data(self): data = { 'alias': 'X3', diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index b4f1e2fe..b684975e 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -35,7 +35,7 @@ class LibraryActiveView(generics.ListAPIView): ).distinct().order_by('-time_update') else: return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update') - + @extend_schema(tags=['Library']) @extend_schema_view() @@ -195,7 +195,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr def get_permissions(self): ''' Determine permission class. ''' if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple', - 'reset_aliases', 'cst_rename']: + 'reset_aliases', 'cst_rename', 'cst_substitute']: permission_classes = [utils.ObjectOwnerOrAdmin] else: permission_classes = [permissions.AllowAny] @@ -256,6 +256,30 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr } ) + @extend_schema( + summary='substitute constituenta', + tags=['Constituenta'], + request=s.CstSubstituteSerializer, + responses={c.HTTP_200_OK: s.RSFormParseSerializer} + ) + @transaction.atomic + @action(detail=True, methods=['patch'], url_path='cst-substitute') + def cst_substitute(self, request, pk): + ''' Substitute occurrences of constituenta with another one. ''' + schema = self._get_schema() + serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': schema}) + 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'] + ) + schema.item.refresh_from_db() + return Response( + status=c.HTTP_200_OK, + data=s.RSFormParseSerializer(schema.item).data + ) + @extend_schema( summary='delete constituents', tags=['Constituenta'], diff --git a/rsconcept/backend/apps/users/messages.py b/rsconcept/backend/apps/users/messages.py new file mode 100644 index 00000000..b4d6fc2d --- /dev/null +++ b/rsconcept/backend/apps/users/messages.py @@ -0,0 +1,8 @@ +''' Utility: Text messages. ''' +# pylint: skip-file + +def passwordAuthFailed(): + return 'Неизвестное сочетание имени пользователя и пароля' + +def passwordsNotMatch(): + return 'Введенные пароли не совпадают' diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 9cc1e4d6..b8bcc54d 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -5,6 +5,7 @@ from rest_framework import serializers from apps.rsform.models import Subscription from . import models +from . import messages as msg class NonFieldErrorSerializer(serializers.Serializer): @@ -36,17 +37,13 @@ class LoginSerializer(serializers.Serializer): password=password ) if not user: - msg = 'Неправильное сочетание имени пользователя и пароля.' - raise serializers.ValidationError(msg, code='authorization') + raise serializers.ValidationError( + msg.passwordAuthFailed(), + code='authorization' + ) attrs['user'] = user return attrs - def create(self, validated_data): - raise NotImplementedError('unexpected `create()` call') - - def update(self, instance, validated_data): - raise NotImplementedError('unexpected `update()` call') - class AuthSerializer(serializers.Serializer): ''' Serializer: Authorization data. ''' @@ -108,12 +105,6 @@ class ChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True) - def create(self, validated_data): - raise NotImplementedError('unexpected `create()` call') - - def update(self, instance, validated_data): - raise NotImplementedError('unexpected `update()` call') - class SignupSerializer(serializers.ModelSerializer): ''' Serializer: Create user profile. ''' @@ -136,7 +127,9 @@ class SignupSerializer(serializers.ModelSerializer): def validate(self, attrs): if attrs['password'] != attrs['password2']: - raise serializers.ValidationError({"password": "Введенные пароли не совпадают"}) + raise serializers.ValidationError({ + 'password': msg.passwordsNotMatch() + }) return attrs def create(self, validated_data): diff --git a/rsconcept/backend/cctext/conceptapi.py b/rsconcept/backend/cctext/conceptapi.py index 9cd76538..f6f8e2dd 100644 --- a/rsconcept/backend/cctext/conceptapi.py +++ b/rsconcept/backend/cctext/conceptapi.py @@ -1,7 +1,7 @@ ''' Concept API Python functions. -::guarantee:: doesnt raise exceptions and returns workable outputs +::guarantee:: doesn't raise exceptions and returns workable outputs ''' from cctext.rumodel import Morphology from .syntax import RuSyntax @@ -56,9 +56,9 @@ def inflect(text: str, target_grams: str) -> str: return model.inflect(target_set) -def inflect_context(target: str, cntxt_before: str = '', cntxt_after: str = '') -> str: +def inflect_context(target: str, before: str = '', after: str = '') -> str: ''' Inflect text in accordance to context before and after. ''' - return parser.inflect_context(target, cntxt_before, cntxt_after) + return parser.inflect_context(target, before, after) def inflect_substitute(substitute_normal: str, original: str) -> str: diff --git a/rsconcept/backend/cctext/reference.py b/rsconcept/backend/cctext/reference.py index 763a6799..c2733bb5 100644 --- a/rsconcept/backend/cctext/reference.py +++ b/rsconcept/backend/cctext/reference.py @@ -25,11 +25,11 @@ class EntityReference: class SyntacticReference: - ''' Reference to syntactic dependcy on EntityReference. ''' + ''' Reference to syntactic dependency on EntityReference. ''' - def __init__(self, referal_offset: int, text: str): + def __init__(self, referral_offset: int, text: str): self.nominal = text - self.offset = referal_offset + self.offset = referral_offset def get_type(self) -> ReferenceType: return ReferenceType.syntactic diff --git a/rsconcept/backend/cctext/resolver.py b/rsconcept/backend/cctext/resolver.py index 6945982f..7e4a5f8e 100644 --- a/rsconcept/backend/cctext/resolver.py +++ b/rsconcept/backend/cctext/resolver.py @@ -34,31 +34,31 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str: return resolved -def resolve_syntactic(ref: SyntacticReference, index: int, allrefs: list['ResolvedReference']) -> str: +def resolve_syntactic(ref: SyntacticReference, index: int, references: list['ResolvedReference']) -> str: ''' Resolve syntactic reference. ''' offset = ref.offset - mainref: Optional['ResolvedReference'] = None + master: Optional['ResolvedReference'] = None if offset > 0: index += 1 - while index < len(allrefs): - if isinstance(allrefs[index].ref, EntityReference): + while index < len(references): + if isinstance(references[index].ref, EntityReference): if offset == 1: - mainref = allrefs[index] + master = references[index] else: offset -= 1 index += 1 else: index -= 1 while index >= 0: - if isinstance(allrefs[index].ref, EntityReference): + if isinstance(references[index].ref, EntityReference): if offset == -1: - mainref = allrefs[index] + master = references[index] else: offset += 1 index -= 1 - if mainref is None: + if master is None: return f'!Некорректное смещение: {ref.offset}!' - return inflect_dependant(ref.nominal, mainref.resolved) + return inflect_dependant(ref.nominal, master.resolved) @dataclass diff --git a/rsconcept/backend/cctext/ruparser.py b/rsconcept/backend/cctext/ruparser.py index fbeae269..3eff35e2 100644 --- a/rsconcept/backend/cctext/ruparser.py +++ b/rsconcept/backend/cctext/ruparser.py @@ -149,7 +149,7 @@ class PhraseParser: _SINGLE_SCORE_SEARCH = 0.2 _PRIORITY_NONE = NO_COORDINATION - _MAIN_WAIT_LIMIT = 10 # count words untill fixing main + _MAIN_WAIT_LIMIT = 10 # count words until fixing main _MAIN_MAX_FOLLOWERS = 3 # count words after main as coordination candidates def parse(self, text: str, @@ -194,7 +194,7 @@ class PhraseParser: return (start, token.stop) return (0, 0) - def inflect_context(self, text: str, cntxt_before: str = '', cntxt_after: str = '') -> str: + def inflect_context(self, text: str, before: str = '', after: str = '') -> str: ''' Inflect text in accordance to context before and after. ''' target = self.parse(text) if not target: @@ -203,8 +203,8 @@ class PhraseParser: if not target_morpho or not target_morpho.can_coordinate: return text - model_after = self.parse(cntxt_after) - model_before = self.parse(cntxt_before) + model_after = self.parse(after) + model_before = self.parse(before) etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after) if not etalon: return text diff --git a/rsconcept/backend/cctext/tests/t_resolver.py b/rsconcept/backend/cctext/tests/t_resolver.py index 1c34b14a..cf3b8202 100644 --- a/rsconcept/backend/cctext/tests/t_resolver.py +++ b/rsconcept/backend/cctext/tests/t_resolver.py @@ -40,14 +40,14 @@ class TestResolver(unittest.TestCase): def test_resolve_syntactic(self): ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку') allrefs = [ref, ref, ref, ref] - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 0, allrefs), '!Некорректное смещение: -1!') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 3, allrefs), '!Некорректное смещение: 1!') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 0, allrefs), 'умному') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=2), 0, allrefs), 'умному') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=3), 0, allrefs), 'умному') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 3, allrefs), 'умному') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-2), 3, allrefs), 'умному') - self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-3), 3, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 0, allrefs), '!Некорректное смещение: -1!') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 3, allrefs), '!Некорректное смещение: 1!') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 0, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=2), 0, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=3), 0, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 3, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-2), 3, allrefs), 'умному') + self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-3), 3, allrefs), 'умному') def test_resolve_invalid(self): self.assertEqual(self.resolver.resolve(''), '')