From 890e676a2cd6641ea23d7b3baa8d08ac2b1b1e6a Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:57:25 +0300 Subject: [PATCH] Implement frontend text refs --- rsconcept/backend/apps/rsform/serializers.py | 46 ++++++++++- .../backend/apps/rsform/tests/t_views.py | 32 ++++++++ rsconcept/backend/apps/rsform/views.py | 15 +++- rsconcept/backend/cctext/__init__.py | 2 +- .../src/components/Common/MiniButton.tsx | 2 +- .../src/components/Common/ReferenceInput.tsx | 76 +++++++++++++++++++ .../src/components/Common/TextArea.tsx | 2 +- .../frontend/src/components/RSInput/index.tsx | 2 +- .../frontend/src/hooks/useResolveText.ts | 31 ++++++++ rsconcept/frontend/src/pages/HomePage.tsx | 4 +- .../pages/RSFormPage/EditorConstituenta.tsx | 9 ++- rsconcept/frontend/src/utils/backendAPI.ts | 10 ++- rsconcept/frontend/src/utils/models.ts | 42 +++++++++- 13 files changed, 258 insertions(+), 15 deletions(-) create mode 100644 rsconcept/frontend/src/components/Common/ReferenceInput.tsx create mode 100644 rsconcept/frontend/src/hooks/useResolveText.ts diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 9fc232be..7b967d02 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -3,6 +3,8 @@ from typing import Optional, cast from rest_framework import serializers from django.db import transaction +from cctext import Resolver, Reference, ReferenceType, EntityReference, SyntacticReference + from .utils import fix_old_references from .models import Constituenta, RSForm @@ -23,6 +25,11 @@ class ExpressionSerializer(serializers.Serializer): expression = serializers.CharField() +class TextSerializer(serializers.Serializer): + ''' Serializer: Text with references. ''' + text = serializers.CharField() + + class RSFormMetaSerializer(serializers.ModelSerializer): ''' Serializer: General purpose RSForm data. ''' class Meta: @@ -288,7 +295,7 @@ class CstRenameSerializer(serializers.ModelSerializer): return attrs -class CstListSerlializer(serializers.Serializer): +class CstListSerializer(serializers.Serializer): ''' Serializer: List of constituents from one origin. ''' items = serializers.ListField( child=CstStandaloneSerializer() @@ -307,6 +314,41 @@ class CstListSerlializer(serializers.Serializer): return attrs -class CstMoveSerlializer(CstListSerlializer): +class CstMoveSerializer(CstListSerializer): ''' Serializer: Change constituenta position. ''' move_to = serializers.IntegerField() + + +class ResolverSerializer(serializers.Serializer): + ''' Serializer: Resolver results serializer. ''' + def to_representation(self, instance: Resolver) -> dict: + return { + 'input': instance.input, + 'output': instance.output, + 'refs': [{ + 'type': str(ref.ref.get_type()), + 'data': self._get_reference_data(ref.ref), + 'resolved': ref.resolved, + 'pos_input': { + 'start': ref.pos_input.start, + 'finish': ref.pos_input.finish + }, + 'pos_output': { + 'start': ref.pos_output.start, + 'finish': ref.pos_output.finish + } + } for ref in instance.refs] + } + + @staticmethod + def _get_reference_data(ref: Reference) -> dict: + if ref.get_type() == ReferenceType.entity: + return { + 'entity': cast(EntityReference, ref).entity, + 'form': cast(EntityReference, ref).form + } + else: + return { + 'offset': cast(SyntacticReference, ref).offset, + 'nominal': cast(SyntacticReference, ref).nominal + } \ No newline at end of file diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index 223fddcf..c5a68a14 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -6,6 +6,8 @@ from zipfile import ZipFile from rest_framework.test import APITestCase, APIRequestFactory, APIClient from rest_framework.exceptions import ErrorDetail +from cctext import ReferenceType + from apps.users.models import User from apps.rsform.models import Syntax, RSForm, Constituenta, CstType from apps.rsform.views import ( @@ -20,6 +22,7 @@ def _response_contains(response, schema: RSForm) -> bool: class TestConstituentaAPI(APITestCase): + ''' Testing constituenta view. ''' def setUp(self): self.factory = APIRequestFactory() self.user = User.objects.create(username='UserTest') @@ -100,6 +103,7 @@ class TestConstituentaAPI(APITestCase): class TestRSFormViewset(APITestCase): + ''' Testing RSForm view. ''' def setUp(self): self.factory = APIRequestFactory() self.user = User.objects.create(username='UserTest') @@ -189,6 +193,34 @@ class TestRSFormViewset(APITestCase): self.assertEqual(response.data['typification'], 'LOGIC') self.assertEqual(response.data['valueClass'], 'value') + def test_resolve(self): + schema = RSForm.objects.create(title='Test') + x1 = schema.insert_at(1, 'X1', CstType.BASE) + x1.term_resolved = 'синий слон' + x1.save() + data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'}) + response = self.client.post(f'/api/rsforms/{schema.id}/resolve/', data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') + self.assertEqual(response.data['output'], 'редким синим слонам') + self.assertEqual(len(response.data['refs']), 2) + self.assertEqual(response.data['refs'][0]['type'], str(ReferenceType.syntactic)) + self.assertEqual(response.data['refs'][0]['resolved'], 'редким') + self.assertEqual(response.data['refs'][0]['data']['offset'], 1) + self.assertEqual(response.data['refs'][0]['data']['nominal'], 'редкий') + self.assertEqual(response.data['refs'][0]['pos_input']['start'], 0) + self.assertEqual(response.data['refs'][0]['pos_input']['finish'], 11) + self.assertEqual(response.data['refs'][0]['pos_output']['start'], 0) + self.assertEqual(response.data['refs'][0]['pos_output']['finish'], 6) + self.assertEqual(response.data['refs'][1]['type'], str(ReferenceType.entity)) + self.assertEqual(response.data['refs'][1]['resolved'], 'синим слонам') + self.assertEqual(response.data['refs'][1]['data']['entity'], 'X1') + self.assertEqual(response.data['refs'][1]['data']['form'], 'plur,datv') + self.assertEqual(response.data['refs'][1]['pos_input']['start'], 12) + self.assertEqual(response.data['refs'][1]['pos_input']['finish'], 27) + self.assertEqual(response.data['refs'][1]['pos_output']['start'], 7) + self.assertEqual(response.data['refs'][1]['pos_output']['finish'], 19) + def test_import_trs(self): work_dir = os.path.dirname(os.path.abspath(__file__)) with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index ff43276b..f85deb5e 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -111,7 +111,7 @@ class RSFormViewSet(viewsets.ModelViewSet): def cst_multidelete(self, request, pk): ''' Endpoint: Delete multiple constituents. ''' schema = self._get_schema() - serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema}) + serializer = serializers.CstListSerializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) schema.delete_cst(serializer.validated_data['constituents']) schema.refresh_from_db() @@ -121,7 +121,7 @@ class RSFormViewSet(viewsets.ModelViewSet): def cst_moveto(self, request, pk): ''' Endpoint: Move multiple constituents. ''' schema = self._get_schema() - serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema}) + serializer = serializers.CstMoveSerializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) schema.move_cst(serializer.validated_data['constituents'], serializer.validated_data['move_to']) schema.refresh_from_db() @@ -200,6 +200,17 @@ class RSFormViewSet(viewsets.ModelViewSet): expression = serializer.validated_data['expression'] result = pyconcept.check_expression(json.dumps(schema.data), expression) return Response(json.loads(result)) + + @action(detail=True, methods=['post']) + def resolve(self, request, pk): + ''' Endpoint: Resolve refenrces in text against schema terms context. ''' + schema = self._get_schema() + serializer = serializers.TextSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + text = serializer.validated_data['text'] + resolver = schema.resolver() + resolver.resolve(text) + return Response(status=200, data=serializers.ResolverSerializer(resolver).data) @action(detail=True, methods=['get'], url_path='export-trs') def export_trs(self, request, pk): diff --git a/rsconcept/backend/cctext/__init__.py b/rsconcept/backend/cctext/__init__.py index 250dc151..bf5bcf0f 100644 --- a/rsconcept/backend/cctext/__init__.py +++ b/rsconcept/backend/cctext/__init__.py @@ -5,7 +5,7 @@ from .rumodel import Morphology, SemanticRole, WordTag, morpho, split_grams, com from .ruparser import PhraseParser, WordToken, Collation from .reference import EntityReference, ReferenceType, SyntacticReference, parse_reference from .context import TermForm, Entity, TermContext -from .resolver import Position, Resolver, ResolvedReference, resolve_entity, resolve_syntactic, extract_entities +from .resolver import Reference, Position, Resolver, ResolvedReference, resolve_entity, resolve_syntactic, extract_entities from .conceptapi import ( parse, normalize, diff --git a/rsconcept/frontend/src/components/Common/MiniButton.tsx b/rsconcept/frontend/src/components/Common/MiniButton.tsx index 32da0b46..1b93089f 100644 --- a/rsconcept/frontend/src/components/Common/MiniButton.tsx +++ b/rsconcept/frontend/src/components/Common/MiniButton.tsx @@ -9,7 +9,7 @@ function MiniButton({ icon, tooltip, children, noHover, ...props }: MiniButtonPr return (