diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 18bfcd0f..32b4957a 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -12,9 +12,9 @@ from django.core.exceptions import ValidationError from django.urls import reverse from apps.users.models import User -from cctext import Resolver, Entity, extract_entities +from cctext import Resolver, Entity, extract_entities, split_grams, TermForm from .graph import Graph -from .utils import apply_mapping_pattern +from .utils import apply_pattern _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}') @@ -273,7 +273,14 @@ class RSForm: ''' Create resolver for text references based on schema terms. ''' result = Resolver({}) for cst in self.constituents(): - entity = Entity(alias=cst.alias, nominal=cst.term_resolved, manual_forms=cst.term_forms) + entity = Entity( + alias=cst.alias, + nominal=cst.term_resolved, + manual_forms=[ + TermForm(text=form['text'], grams=split_grams(form['tags'])) + for form in cst.term_forms + ] + ) result.context[cst.alias] = entity return result @@ -314,7 +321,10 @@ class RSForm: raise ValidationError('Invalid position: should be positive integer') currentSize = self.constituents().count() position = max(1, min(position, currentSize + 1)) - update_list = Constituenta.objects.only('id', 'order', 'schema').filter(schema=self.item, order__gte=position) + update_list = \ + Constituenta.objects \ + .only('id', 'order', 'schema') \ + .filter(schema=self.item, order__gte=position) for cst in update_list: cst.order += 1 Constituenta.objects.bulk_update(update_list, ['order']) @@ -424,19 +434,19 @@ class RSForm: 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) + expression = apply_pattern(cst.definition_formal, mapping, _GLOBAL_ID_PATTERN) if expression != cst.definition_formal: modified = True cst.definition_formal = expression - convention = apply_mapping_pattern(cst.convention, mapping, _GLOBAL_ID_PATTERN) + convention = apply_pattern(cst.convention, mapping, _GLOBAL_ID_PATTERN) if convention != cst.convention: modified = True cst.convention = convention - term = apply_mapping_pattern(cst.term_raw, mapping, _REF_ENTITY_PATTERN) + term = apply_pattern(cst.term_raw, mapping, _REF_ENTITY_PATTERN) if term != cst.term_raw: modified = True cst.term_raw = term - definition = apply_mapping_pattern(cst.definition_raw, mapping, _REF_ENTITY_PATTERN) + definition = apply_pattern(cst.definition_raw, mapping, _REF_ENTITY_PATTERN) if definition != cst.definition_raw: modified = True cst.definition_raw = definition diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 72b380e6..c6204ec0 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -148,18 +148,19 @@ 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 schema = RSForm(instance.schema) - definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None - term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None - term_changed = False + 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 + term_changed = 'term_forms' in data if definition is not None and definition != instance.definition_raw : - validated_data['definition_resolved'] = schema.resolver().resolve(definition) + data['definition_resolved'] = schema.resolver().resolve(definition) if term is not None and term != instance.term_raw: - validated_data['term_resolved'] = schema.resolver().resolve(term) - if validated_data['term_resolved'] != instance.term_resolved: - validated_data['term_forms'] = [] - term_changed = validated_data['term_resolved'] != instance.term_resolved - result: Constituenta = super().update(instance, validated_data) + data['term_resolved'] = schema.resolver().resolve(term) + if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data: + data['term_forms'] = [] + term_changed = data['term_resolved'] != instance.term_resolved + result: Constituenta = super().update(instance, data) if term_changed: schema.on_term_change([result.alias]) result.refresh_from_db() diff --git a/rsconcept/backend/apps/rsform/tests/t_utils.py b/rsconcept/backend/apps/rsform/tests/t_utils.py index 21fa63c6..2f08cd8d 100644 --- a/rsconcept/backend/apps/rsform/tests/t_utils.py +++ b/rsconcept/backend/apps/rsform/tests/t_utils.py @@ -2,7 +2,7 @@ import unittest import re -from apps.rsform.utils import apply_mapping_pattern, fix_old_references +from apps.rsform.utils import apply_pattern, fix_old_references class TestUtils(unittest.TestCase): @@ -10,10 +10,10 @@ class TestUtils(unittest.TestCase): def test_apply_mapping_patter(self): mapping = {'X101': 'X20'} pattern = re.compile(r'(X[0-9]+)') - self.assertEqual(apply_mapping_pattern('', mapping, pattern), '') - self.assertEqual(apply_mapping_pattern('X20', mapping, pattern), 'X20') - self.assertEqual(apply_mapping_pattern('X101', mapping, pattern), 'X20') - self.assertEqual(apply_mapping_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf') + self.assertEqual(apply_pattern('', mapping, pattern), '') + self.assertEqual(apply_pattern('X20', mapping, pattern), 'X20') + self.assertEqual(apply_pattern('X101', mapping, pattern), 'X20') + self.assertEqual(apply_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf') def test_fix_old_references(self): self.assertEqual(fix_old_references(''), '') diff --git a/rsconcept/backend/apps/rsform/tests/t_views.py b/rsconcept/backend/apps/rsform/tests/t_views.py index b55d077e..38218cd2 100644 --- a/rsconcept/backend/apps/rsform/tests/t_views.py +++ b/rsconcept/backend/apps/rsform/tests/t_views.py @@ -1,5 +1,4 @@ ''' Testing views ''' -import json import os import io from zipfile import ZipFile @@ -52,30 +51,43 @@ class TestConstituentaAPI(APITestCase): self.assertEqual(response.data['convention'], self.cst1.convention) def test_partial_update(self): - data = json.dumps({'convention': 'tt'}) - response = self.client.patch(f'/api/constituents/{self.cst2.id}', data, content_type='application/json') + data = {'convention': 'tt'} + response = self.client.patch( + f'/api/constituents/{self.cst2.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) self.client.logout() - response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json') + response = self.client.patch( + f'/api/constituents/{self.cst1.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) self.client.force_authenticate(user=self.user) - response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json') + response = self.client.patch( + f'/api/constituents/{self.cst1.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.cst1.refresh_from_db() self.assertEqual(response.data['convention'], 'tt') self.assertEqual(self.cst1.convention, 'tt') - response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json') + response = self.client.patch( + f'/api/constituents/{self.cst1.id}', + data=data, + format='json' + ) self.assertEqual(response.status_code, 200) def test_update_resolved_norefs(self): - data = json.dumps({ + data = { 'term_raw': 'New term', 'definition_raw': 'New def' - }) - response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, content_type='application/json') + } + response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, format='json') self.assertEqual(response.status_code, 200) self.cst3.refresh_from_db() self.assertEqual(response.data['term_resolved'], 'New term') @@ -84,11 +96,14 @@ class TestConstituentaAPI(APITestCase): self.assertEqual(self.cst3.definition_resolved, 'New def') def test_update_resolved_refs(self): - data = json.dumps({ + data = { 'term_raw': '@{X1|nomn,sing}', 'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}' - }) - response = self.client.patch(f'/api/constituents/{self.cst3.id}', data, content_type='application/json') + } + response = self.client.patch( + f'/api/constituents/{self.cst3.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.cst3.refresh_from_db() self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) @@ -97,8 +112,11 @@ class TestConstituentaAPI(APITestCase): self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1') def test_readonly_cst_fields(self): - data = json.dumps({'alias': 'X33', 'order': 10}) - response = self.client.patch(f'/api/constituents/{self.cst1.id}', data, content_type='application/json') + data = {'alias': 'X33', 'order': 10} + response = self.client.patch( + f'/api/constituents/{self.cst1.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['alias'], 'X1') self.assertEqual(response.data['alias'], self.cst1.alias) @@ -132,29 +150,33 @@ class TestLibraryViewset(APITestCase): def test_create_anonymous(self): self.client.logout() - data = json.dumps({'title': 'Title'}) - response = self.client.post('/api/library', data=data, content_type='application/json') + data = {'title': 'Title'} + response = self.client.post('/api/library', data=data, format='json') self.assertEqual(response.status_code, 403) def test_create_populate_user(self): - data = json.dumps({'title': 'Title'}) - response = self.client.post('/api/library', data=data, content_type='application/json') + data = {'title': 'Title'} + response = self.client.post('/api/library', data=data, format='json') self.assertEqual(response.status_code, 201) self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['owner'], self.user.id) def test_update(self): - data = json.dumps({'id': self.owned.id, 'title': 'New title'}) - response = self.client.patch(f'/api/library/{self.owned.id}', - data=data, content_type='application/json') + data = {'id': self.owned.id, 'title': 'New title'} + response = self.client.patch( + f'/api/library/{self.owned.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['title'], 'New title') self.assertEqual(response.data['alias'], self.owned.alias) def test_update_unowned(self): - data = json.dumps({'id': self.unowned.id, 'title': 'New title'}) - response = self.client.patch(f'/api/library/{self.unowned.id}', - data=data, content_type='application/json') + data = {'id': self.unowned.id, 'title': 'New title'} + response = self.client.patch( + f'/api/library/{self.unowned.id}', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) def test_destroy(self): @@ -304,8 +326,11 @@ class TestRSFormViewset(APITestCase): def test_check(self): schema = RSForm.create(title='Test') schema.insert_at(1, 'X1', CstType.BASE) - data = json.dumps({'expression': 'X1=X1'}) - response = self.client.post(f'/api/rsforms/{schema.item.id}/check', data=data, content_type='application/json') + data = {'expression': 'X1=X1'} + response = self.client.post( + f'/api/rsforms/{schema.item.id}/check', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['syntax'], Syntax.MATH) @@ -318,8 +343,11 @@ class TestRSFormViewset(APITestCase): 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.item.id}/resolve', data=data, content_type='application/json') + data = {'text': '@{1|редкий} @{X1|plur,datv}'} + response = self.client.post( + f'/api/rsforms/{schema.item.id}/resolve', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') self.assertEqual(response.data['output'], 'редким синим слонам') @@ -362,24 +390,30 @@ class TestRSFormViewset(APITestCase): self.assertIn('document.json', zipped_file.namelist()) def test_create_constituenta(self): - data = json.dumps({'alias': 'X3', 'cst_type': 'basic'}) - response = self.client.post(f'/api/rsforms/{self.unowned.item.id}/cst-create', - data=data, content_type='application/json') + data = {'alias': 'X3', 'cst_type': 'basic'} + response = self.client.post( + f'/api/rsforms/{self.unowned.item.id}/cst-create', + data=data, format='json' + ) 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) - response = self.client.post(f'/api/rsforms/{item.id}/cst-create', - data=data, content_type='application/json') + response = self.client.post( + f'/api/rsforms/{item.id}/cst-create', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['new_cst']['alias'], 'X3') x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) self.assertEqual(x3.order, 3) - data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id}) - response = self.client.post(f'/api/rsforms/{item.id}/cst-create', - data=data, content_type='application/json') + data = {'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id} + response = self.client.post( + f'/api/rsforms/{item.id}/cst-create', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['new_cst']['alias'], 'X4') x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) @@ -389,30 +423,39 @@ class TestRSFormViewset(APITestCase): self.cst1 = Constituenta.objects.create( alias='X1', schema=self.owned.item, order=1, convention='Test', term_raw='Test1', term_resolved='Test1', - term_forms=[{'text':'form1', 'tags':'sing,datv'}]) + term_forms=[{'text':'form1', 'tags':'sing,datv'}] + ) self.cst2 = Constituenta.objects.create( alias='X2', schema=self.unowned.item, order=1, convention='Test1', - term_raw='Test2', term_resolved='Test2') + term_raw='Test2', term_resolved='Test2' + ) self.cst3 = Constituenta.objects.create( alias='X3', schema=self.owned.item, order=2, term_raw='Test3', term_resolved='Test3', - definition_raw='Test1', definition_resolved='Test2') + 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.unowned.item.id}/cst-rename', - data=data, content_type='application/json') + data = {'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk} + response = self.client.patch( + f'/api/rsforms/{self.unowned.item.id}/cst-rename', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) - response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename', - data=data, content_type='application/json') + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-rename', + data=data, format='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.owned.item.id}/cst-rename', - data=data, content_type='application/json') + data = {'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk} + response = self.client.patch( + f'/api/rsforms/{self.owned.item.id}/cst-rename', + data=data, format='json' + ) self.assertEqual(response.status_code, 400) - data = json.dumps({'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk}) + data = {'alias': 'D2', 'cst_type': 'term', 'id': self.cst1.pk} item = self.owned.item d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4) d1.term_raw = '@{X1|plur}' @@ -423,8 +466,10 @@ class TestRSFormViewset(APITestCase): 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/{item.id}/cst-rename', - data=data, content_type='application/json') + response = self.client.patch( + f'/api/rsforms/{item.id}/cst-rename', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['new_cst']['alias'], 'D2') self.assertEqual(response.data['new_cst']['cst_type'], 'term') @@ -438,17 +483,19 @@ class TestRSFormViewset(APITestCase): self.assertEqual(self.cst1.cst_type, CstType.TERM) def test_create_constituenta_data(self): - data = json.dumps({ + data = { 'alias': 'X3', 'cst_type': 'basic', 'convention': '1', 'term_raw': '2', 'definition_formal': '3', 'definition_raw': '4' - }) + } item = self.owned.item - response = self.client.post(f'/api/rsforms/{item.id}/cst-create', - data=data, content_type='application/json') + response = self.client.post( + f'/api/rsforms/{item.id}/cst-create', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['cst_type'], 'basic') @@ -461,16 +508,20 @@ class TestRSFormViewset(APITestCase): def test_delete_constituenta(self): schema = self.owned - data = json.dumps({'items': [1337]}) - response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', - data=data, content_type='application/json') + data = {'items': [1337]} + response = self.client.patch( + f'/api/rsforms/{schema.item.id}/cst-multidelete', + data=data, format='json' + ) self.assertEqual(response.status_code, 400) x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1) x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2) - data = json.dumps({'items': [x1.id]}) - response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', - data=data, content_type='application/json') + data = {'items': [x1.id]} + response = self.client.patch( + f'/api/rsforms/{schema.item.id}/cst-multidelete', + data=data, format='json' + ) x2.refresh_from_db() schema.item.refresh_from_db() self.assertEqual(response.status_code, 202) @@ -480,23 +531,29 @@ class TestRSFormViewset(APITestCase): self.assertEqual(x2.order, 1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1) - data = json.dumps({'items': [x3.id]}) - response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', - data=data, content_type='application/json') + data = {'items': [x3.id]} + response = self.client.patch( + f'/api/rsforms/{schema.item.id}/cst-multidelete', + data=data, format='json' + ) self.assertEqual(response.status_code, 400) def test_move_constituenta(self): item = self.owned.item - data = json.dumps({'items': [1337], 'move_to': 1}) - response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', - data=data, content_type='application/json') + data = {'items': [1337], 'move_to': 1} + response = self.client.patch( + f'/api/rsforms/{item.id}/cst-moveto', + data=data, format='json' + ) self.assertEqual(response.status_code, 400) x1 = 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) - data = json.dumps({'items': [x2.id], 'move_to': 1}) - response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', - data=data, content_type='application/json') + data = {'items': [x2.id], 'move_to': 1} + response = self.client.patch( + f'/api/rsforms/{item.id}/cst-moveto', + data=data, format='json' + ) x1.refresh_from_db() x2.refresh_from_db() self.assertEqual(response.status_code, 200) @@ -505,9 +562,11 @@ class TestRSFormViewset(APITestCase): self.assertEqual(x2.order, 1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1) - data = json.dumps({'items': [x3.id], 'move_to': 1}) - response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', - data=data, content_type='application/json') + data = {'items': [x3.id], 'move_to': 1} + response = self.client.patch( + f'/api/rsforms/{item.id}/cst-moveto', + data=data, format='json' + ) self.assertEqual(response.status_code, 400) def test_reset_aliases(self): @@ -542,7 +601,10 @@ class TestRSFormViewset(APITestCase): work_dir = os.path.dirname(os.path.abspath(__file__)) with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: data = {'file': file, 'load_metadata': False} - response = self.client.patch(f'/api/rsforms/{schema.item.id}/load-trs', data=data, format='multipart') + response = self.client.patch( + f'/api/rsforms/{schema.item.id}/load-trs', + data=data, format='multipart' + ) schema.item.refresh_from_db() self.assertEqual(response.status_code, 200) self.assertEqual(schema.item.title, 'Testt11') @@ -563,8 +625,11 @@ class TestRSFormViewset(APITestCase): x1.save() d1.save() - data = json.dumps({'title': 'Title'}) - response = self.client.post(f'/api/library/{item.id}/clone', data=data, content_type='application/json') + data = {'title': 'Title'} + response = self.client.post( + f'/api/library/{item.id}/clone', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['title'], 'Title') @@ -586,7 +651,10 @@ class TestRSLanguageViews(APITestCase): work_dir = os.path.dirname(os.path.abspath(__file__)) with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'} - response = self.client.post('/api/rsforms/create-detailed', data=data, format='multipart') + response = self.client.post( + '/api/rsforms/create-detailed', + data=data, format='multipart' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['title'], 'Test123') @@ -595,7 +663,10 @@ class TestRSLanguageViews(APITestCase): def test_create_rsform_fallback(self): data = {'title': 'Test123', 'comment': '123', 'alias': 'ks1'} - response = self.client.post('/api/rsforms/create-detailed', data=data) + response = self.client.post( + '/api/rsforms/create-detailed', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['title'], 'Test123') @@ -604,35 +675,50 @@ class TestRSLanguageViews(APITestCase): def test_convert_to_ascii(self): data = {'expression': '1=1'} - request = self.factory.post('/api/rslang/to-ascii', data) + request = self.factory.post( + '/api/rslang/to-ascii', + data=data, format='json' + ) response = convert_to_ascii(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['result'], r'1 \eq 1') def test_convert_to_ascii_missing_data(self): data = {'data': '1=1'} - request = self.factory.post('/api/rslang/to-ascii', data) + request = self.factory.post( + '/api/rslang/to-ascii', + data=data, format='json' + ) response = convert_to_ascii(request) self.assertEqual(response.status_code, 400) self.assertIsInstance(response.data['expression'][0], ErrorDetail) def test_convert_to_math(self): data = {'expression': r'1 \eq 1'} - request = self.factory.post('/api/rslang/to-math', data) + request = self.factory.post( + '/api/rslang/to-math', + data=data, format='json' + ) response = convert_to_math(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['result'], r'1=1') def test_convert_to_math_missing_data(self): data = {'data': r'1 \eq 1'} - request = self.factory.post('/api/rslang/to-math', data) + request = self.factory.post( + '/api/rslang/to-math', + data=data, format='json' + ) response = convert_to_math(request) self.assertEqual(response.status_code, 400) self.assertIsInstance(response.data['expression'][0], ErrorDetail) def test_parse_expression(self): data = {'expression': r'1=1'} - request = self.factory.post('/api/rslang/parse-expression', data) + request = self.factory.post( + '/api/rslang/parse-expression', + data=data, format='json' + ) response = parse_expression(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['parseResult'], True) @@ -641,7 +727,10 @@ class TestRSLanguageViews(APITestCase): def test_parse_expression_missing_data(self): data = {'data': r'1=1'} - request = self.factory.post('/api/rslang/parse-expression', data) + request = self.factory.post( + '/api/rslang/parse-expression', + data=data, format='json' + ) response = parse_expression(request) self.assertEqual(response.status_code, 400) self.assertIsInstance(response.data['expression'][0], ErrorDetail) @@ -657,21 +746,30 @@ class TestNaturalLanguageViews(APITestCase): def test_parse_text(self): data = {'text': 'синим слонам'} - request = self.factory.post('/api/cctext/parse', data) + request = self.factory.post( + '/api/cctext/parse', + data=data, format='json' + ) response = parse_text(request) self.assertEqual(response.status_code, 200) self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc') def test_inflect(self): data = {'text': 'синий слон', 'grams': 'plur,datv'} - request = self.factory.post('/api/cctext/inflect', data) + request = self.factory.post( + '/api/cctext/inflect', + data=data, format='json' + ) response = inflect(request) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['result'], 'синим слонам') def test_generate_lexeme(self): data = {'text': 'синий слон'} - request = self.factory.post('/api/cctext/generate-lexeme', data) + request = self.factory.post( + '/api/cctext/generate-lexeme', + data=data, format='json' + ) response = generate_lexeme(request) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data['items']), 12) diff --git a/rsconcept/backend/apps/rsform/utils.py b/rsconcept/backend/apps/rsform/utils.py index 819dfdaa..366ce778 100644 --- a/rsconcept/backend/apps/rsform/utils.py +++ b/rsconcept/backend/apps/rsform/utils.py @@ -50,7 +50,7 @@ def write_trs(json_data: dict) -> bytes: archive.writestr('document.json', data=data) return content.getvalue() -def apply_mapping_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str: +def apply_pattern(text: str, mapping: dict[str, str], pattern: re.Pattern[str]) -> str: ''' Apply mapping to matching in regular expression patter subgroup 1. ''' if text == '' or pattern == '': return text diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 32c650f7..f8527479 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -202,7 +202,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr serializer = s.CstCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data - new_cst = schema.create_cst(data, data['insert_after'] if 'insert_after' in data else None) + new_cst = schema.create_cst( + data=data, + insert_after=data['insert_after'] if 'insert_after' in data else None + ) schema.item.refresh_from_db() response = Response( status=c.HTTP_201_CREATED, @@ -251,7 +254,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr def cst_multidelete(self, request, pk): ''' Endpoint: Delete multiple constituents. ''' schema = self._get_schema() - serializer = s.CstListSerializer(data=request.data, context={'schema': schema}) + serializer = s.CstListSerializer( + data=request.data, + context={'schema': schema} + ) serializer.is_valid(raise_exception=True) schema.delete_cst(serializer.validated_data['constituents']) schema.item.refresh_from_db() @@ -270,9 +276,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr def cst_moveto(self, request, pk): ''' Endpoint: Move multiple constituents. ''' schema = self._get_schema() - serializer = s.CstMoveSerializer(data=request.data, context={'schema': schema}) + serializer = s.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.move_cst( + listCst=serializer.validated_data['constituents'], + target=serializer.validated_data['move_to'] + ) schema.item.refresh_from_db() return Response( status=c.HTTP_200_OK, @@ -311,7 +323,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr data = utils.read_trs(request.FILES['file'].file) data['id'] = schema.item.pk - serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata}) + serializer = s.RSFormTRSSerializer( + data=data, + context={'load_meta': load_metadata} + ) serializer.is_valid(raise_exception=True) schema = serializer.save() return Response( @@ -427,7 +442,10 @@ class TrsImportView(views.APIView): if owner.is_anonymous: owner = None _prepare_rsform_data(data, request, owner) - serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) + serializer = s.RSFormTRSSerializer( + data=data, + context={'load_meta': True} + ) serializer.is_valid(raise_exception=True) schema = serializer.save() result = s.LibraryItemSerializer(schema.item) @@ -575,7 +593,7 @@ def inflect(request): @extend_schema( - summary='basic set of wordforms', + summary='all wordforms for current lexeme', tags=['NaturalLanguage'], request=s.TextSerializer, responses={200: s.MultiFormSerializer}, @@ -583,7 +601,7 @@ def inflect(request): ) @api_view(['POST']) def generate_lexeme(request): - ''' Endpoint: Generate basic set of wordforms. ''' + ''' Endpoint: Generate complete set of wordforms for lexeme. ''' serializer = s.TextSerializer(data=request.data) serializer.is_valid(raise_exception=True) nominal = serializer.validated_data['text'] @@ -595,7 +613,7 @@ def generate_lexeme(request): @extend_schema( - summary='get all language parse variants', + summary='get likely parse grammemes', tags=['NaturalLanguage'], request=s.TextSerializer, responses={200: s.ResultTextResponse}, diff --git a/rsconcept/backend/apps/users/tests/t_views.py b/rsconcept/backend/apps/users/tests/t_views.py index 1f755911..cd7b554a 100644 --- a/rsconcept/backend/apps/users/tests/t_views.py +++ b/rsconcept/backend/apps/users/tests/t_views.py @@ -1,12 +1,10 @@ ''' Testing views ''' -import json from rest_framework.test import APITestCase, APIClient from apps.users.models import User from apps.rsform.models import LibraryItem, LibraryItemType -# TODO: test ACTIVE_USERS class TestUserAPIViews(APITestCase): def setUp(self): self.username = 'UserTest' @@ -18,8 +16,11 @@ class TestUserAPIViews(APITestCase): self.client = APIClient() def test_login(self): - data = json.dumps({'username': self.username, 'password': self.password}) - response = self.client.post('/users/api/login', data=data, content_type='application/json') + data = {'username': self.username, 'password': self.password} + response = self.client.post( + '/users/api/login', + data=data, format='json' + ) self.assertEqual(response.status_code, 202) def test_logout(self): @@ -81,12 +82,15 @@ class TestUserUserProfileAPIView(APITestCase): def test_patch_profile(self): self.client.force_login(user=self.user) - data = json.dumps({ + data = { 'email': '123@mail.ru', 'first_name': 'firstName', 'last_name': 'lastName', - }) - response = self.client.patch('/users/api/profile', data=data, content_type='application/json') + } + response = self.client.patch( + '/users/api/profile', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['email'], '123@mail.ru') self.assertEqual(response.data['first_name'], 'firstName') @@ -94,31 +98,52 @@ class TestUserUserProfileAPIView(APITestCase): def test_edit_profile(self): newmail = 'newmail@gmail.com' - data = json.dumps({'email': newmail}) - response = self.client.patch('/users/api/profile', data, content_type='application/json') + data = {'email': newmail} + response = self.client.patch( + '/users/api/profile', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) self.client.force_login(user=self.user) - response = self.client.patch('/users/api/profile', data, content_type='application/json') + response = self.client.patch( + '/users/api/profile', + data=data, format='json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['username'], self.username) self.assertEqual(response.data['email'], newmail) def test_change_password(self): newpassword = 'pw2' - data = json.dumps({'old_password': self.password, 'new_password': newpassword}) - response = self.client.patch('/users/api/change-password', data, content_type='application/json') + data = { + 'old_password': self.password, + 'new_password': newpassword + } + response = self.client.patch( + '/users/api/change-password', + data=data, format='json' + ) self.assertEqual(response.status_code, 403) self.assertFalse(self.client.login(username=self.user.username, password=newpassword)) self.assertTrue(self.client.login(username=self.user.username, password=self.password)) - invalid = json.dumps({'old_password': 'invalid', 'new_password': newpassword}) - response = self.client.patch('/users/api/change-password', invalid, content_type='application/json') + invalid = { + 'old_password': 'invalid', + 'new_password': newpassword + } + response = self.client.patch( + '/users/api/change-password', + data=invalid, format='json' + ) self.assertEqual(response.status_code, 400) oldHash = self.user.password self.client.force_login(user=self.user) - response = self.client.patch('/users/api/change-password', data, content_type='application/json') + response = self.client.patch( + '/users/api/change-password', + data=data, format='json' + ) self.user.refresh_from_db() self.assertEqual(response.status_code, 204) self.assertNotEqual(self.user.password, oldHash) @@ -131,15 +156,18 @@ class TestSignupAPIView(APITestCase): self.client = APIClient() def test_signup(self): - data = json.dumps({ + data = { 'username': 'TestUser', 'email': 'email@mail.ru', 'password': 'Test@@123', 'password2': 'Test@@123', 'first_name': 'firstName', 'last_name': 'lastName', - }) - response = self.client.post('/users/api/signup', data, content_type='application/json') + } + response = self.client.post( + '/users/api/signup', + data=data, format='json' + ) self.assertEqual(response.status_code, 201) self.assertTrue('id' in response.data) self.assertEqual(response.data['username'], 'TestUser') diff --git a/rsconcept/backend/cctext/context.py b/rsconcept/backend/cctext/context.py index b421f89c..a184055d 100644 --- a/rsconcept/backend/cctext/context.py +++ b/rsconcept/backend/cctext/context.py @@ -1,24 +1,40 @@ ''' Term context for reference resolution. ''' from typing import Iterable, Dict, Optional, TypedDict -from .conceptapi import inflect +from .ruparser import PhraseParser +from .rumodel import WordTag + + +parser = PhraseParser() class TermForm(TypedDict): - ''' Term in a specific form. ''' + ''' Represents term in a specific form. ''' text: str - tags: str + grams: Iterable[str] -def _search_form(query: str, data: Iterable[TermForm]) -> Optional[str]: - for tf in data: - if tf['tags'] == query: - return tf['text'] +def _match_grams(query: Iterable[str], test: Iterable[str]) -> bool: + ''' Check if grams from test fit query. ''' + for gram in test: + if not gram in query: + if not gram in WordTag.PARTS_OF_SPEECH: + return False + for pos in WordTag.PARTS_OF_SPEECH: + if pos in query: + return False + return True + + +def _search_form(query: Iterable[str], data: Iterable[TermForm]) -> Optional[str]: + for form in data: + if _match_grams(query, form['grams']): + return form['text'] return None class Entity: - ''' Text entity. ''' + ''' Represents text entity. ''' def __init__(self, alias: str, nominal: str, manual_forms: Optional[Iterable[TermForm]]=None): if manual_forms is None: self.manual = [] @@ -41,20 +57,28 @@ class Entity: self.manual = [] self._cached = [] - def get_form(self, form: str) -> str: + def get_form(self, grams: Iterable[str]) -> str: ''' Get specific term form. ''' - if form == '': + if all(False for _ in grams): return self._nominal - text = _search_form(form, self.manual) - if text is None: - text = _search_form(form, self._cached) - if text is None: - try: - text = inflect(self._nominal, form) - except ValueError as error: - text = f'!{error}!'.replace('Unknown grammeme', 'Неизвестная граммема') - self._cached.append({'text': text, 'tags': form}) + text = _search_form(grams, self.manual) + if text is not None: + return text + text = _search_form(grams, self._cached) + if text is not None: + return text + + model = parser.parse(self._nominal) + if model is None: + text = self._nominal + else: + try: + text = model.inflect(grams) + except ValueError as error: + text = f'!{error}!'.replace('Unknown grammeme', 'Неизвестная граммема') + self._cached.append({'text': text, 'grams': grams}) return text -# Term context for resolving entity references. + +# Represents term context for resolving entity references. TermContext = Dict[str, Entity] diff --git a/rsconcept/backend/cctext/resolver.py b/rsconcept/backend/cctext/resolver.py index 528d191d..6945982f 100644 --- a/rsconcept/backend/cctext/resolver.py +++ b/rsconcept/backend/cctext/resolver.py @@ -3,6 +3,8 @@ import re from typing import cast, Optional from dataclasses import dataclass +from .rumodel import split_grams + from .conceptapi import inflect_dependant from .context import TermContext from .reference import EntityReference, SyntacticReference, parse_reference, Reference @@ -24,7 +26,8 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str: alias = ref.entity if alias not in context: return f'!Неизвестная сущность: {alias}!' - resolved = context[alias].get_form(ref.form) + grams = split_grams(ref.form) + resolved = context[alias].get_form(grams) if resolved == '': return f'!Отсутствует термин: {alias}!' else: diff --git a/rsconcept/backend/cctext/tests/t_context.py b/rsconcept/backend/cctext/tests/t_context.py index e1e8f540..8d996e9d 100644 --- a/rsconcept/backend/cctext/tests/t_context.py +++ b/rsconcept/backend/cctext/tests/t_context.py @@ -9,24 +9,24 @@ class TestEntity(unittest.TestCase): self.alias = 'X1' self.nominal = 'человек' self.text1 = 'test1' - self.form1 = 'sing,datv' - self.entity = Entity(self.alias, self.nominal, [{'text': self.text1, 'tags': self.form1}]) + self.form1 = ['sing','datv'] + self.entity = Entity(self.alias, self.nominal, [{'text': self.text1, 'grams': self.form1}]) def test_attributes(self): self.assertEqual(self.entity.alias, self.alias) self.assertEqual(self.entity.get_nominal(), self.nominal) - self.assertEqual(self.entity.manual, [{'text': self.text1, 'tags': self.form1}]) + self.assertEqual(self.entity.manual, [{'text': self.text1, 'grams': self.form1}]) def test_get_form(self): - self.assertEqual(self.entity.get_form(''), self.nominal) + self.assertEqual(self.entity.get_form([]), self.nominal) self.assertEqual(self.entity.get_form(self.form1), self.text1) - self.assertEqual(self.entity.get_form('invalid tags'), '!Неизвестная граммема: invalid tags!') - self.assertEqual(self.entity.get_form('plur'), 'люди') + self.assertEqual(self.entity.get_form(['invalid tags']), '!Неизвестная граммема: invalid tags!') + self.assertEqual(self.entity.get_form(['plur']), 'люди') def test_set_nominal(self): new_nomial = 'TEST' - self.assertEqual(self.entity.get_form('plur'), 'люди') + self.assertEqual(self.entity.get_form(['plur']), 'люди') self.entity.set_nominal(new_nomial) self.assertEqual(self.entity.get_nominal(), new_nomial) - self.assertEqual(self.entity.get_form('plur'), new_nomial) + self.assertEqual(self.entity.get_form(['plur']), new_nomial) self.assertEqual(self.entity.manual, []) diff --git a/rsconcept/backend/cctext/tests/t_resolver.py b/rsconcept/backend/cctext/tests/t_resolver.py index e0d88b6d..1c34b14a 100644 --- a/rsconcept/backend/cctext/tests/t_resolver.py +++ b/rsconcept/backend/cctext/tests/t_resolver.py @@ -2,9 +2,11 @@ import unittest from typing import cast +from django.test import tag + from cctext import ( EntityReference, TermContext, Entity, SyntacticReference, - Resolver, ResolvedReference, Position, + Resolver, ResolvedReference, Position, TermForm, resolve_entity, resolve_syntactic, extract_entities ) @@ -88,3 +90,20 @@ class TestResolver(unittest.TestCase): self.assertEqual(self.resolver.refs[1].pos_output, Position(9, 15)) self.assertEqual(self.resolver.refs[2].pos_input, Position(28, 38)) self.assertEqual(self.resolver.refs[2].pos_output, Position(16, 20)) + + def test_resolve_manual_forms(self): + self.context['X1'] = Entity( + alias='X1', + nominal='человек', + manual_forms=[ + TermForm(text='тест1', grams='NOUN,sing'.split(',')), + TermForm(text='тест2', grams='NOUN,datv,plur'.split(',')) + ] + ) + self.assertEqual(self.resolver.resolve('@{X1|NOUN,sing,nomn}'), 'тест1', 'Match subset') + self.assertEqual(self.resolver.resolve('@{X1|NOUN,sing}'), 'тест1', 'Match full') + self.assertEqual(self.resolver.resolve('@{X1|NOUN,datv,plur}'), 'тест2') + self.assertEqual(self.resolver.resolve('@{X1|NOUN,plur,datv}'), 'тест2', 'Match any order') + self.assertEqual(self.resolver.resolve('@{X1|datv,plur}'), 'тест2', 'Match missing POS') + self.assertEqual(self.resolver.resolve('@{X1|NOUN,datv,sing}'), 'тест1') + self.assertEqual(self.resolver.resolve('@{X1|VERB,datv,plur}'), 'человек') diff --git a/rsconcept/frontend/src/components/DataTable/index.tsx b/rsconcept/frontend/src/components/DataTable/index.tsx index 2daad356..ceb4ca4a 100644 --- a/rsconcept/frontend/src/components/DataTable/index.tsx +++ b/rsconcept/frontend/src/components/DataTable/index.tsx @@ -150,7 +150,7 @@ export default function DataTable({ style={conditionalRowStyles && getRowStyles(row)} > {enableRowSelection && - + } {row.getVisibleCells().map( diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx index 7534ca70..8352f7d7 100644 --- a/rsconcept/frontend/src/components/Icons.tsx +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -252,6 +252,22 @@ export function ArrowDownIcon(props: IconProps) { ); } +export function ArrowLeftIcon(props: IconProps) { + return ( + + + + ); +} + +export function ArrowRightIcon(props: IconProps) { + return ( + + + + ); +} + export function CloneIcon(props: IconProps) { return ( @@ -407,6 +423,15 @@ export function ChevronDoubleUpIcon(props: IconProps) { ); } +export function ChevronDoubleDownIcon(props: IconProps) { + return ( + + + + + ); +} + export function CheckIcon(props: IconProps) { return ( diff --git a/rsconcept/frontend/src/hooks/useConceptText.ts b/rsconcept/frontend/src/hooks/useConceptText.ts new file mode 100644 index 00000000..db69d9d5 --- /dev/null +++ b/rsconcept/frontend/src/hooks/useConceptText.ts @@ -0,0 +1,56 @@ +import { useCallback, useState } from 'react' + +import { ErrorInfo } from '../components/BackendError'; +import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '../models/language'; +import { DataCallback, postGenerateLexeme, postInflectText, postParseText } from '../utils/backendAPI'; + +function useConceptText() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + + const inflect = useCallback( + (data: IWordFormPlain, onSuccess: DataCallback) => { + setError(undefined); + postInflectText({ + data: data, + showError: true, + setLoading, + onError: error => setError(error), + onSuccess: data => { + if (onSuccess) onSuccess(data); + } + }); + }, []); + + const parse = useCallback( + (data: ITextRequest, onSuccess: DataCallback) => { + setError(undefined); + postParseText({ + data: data, + showError: true, + setLoading, + onError: error => setError(error), + onSuccess: data => { + if (onSuccess) onSuccess(data); + } + }); + }, []); + + const generateLexeme = useCallback( + (data: ITextRequest, onSuccess: DataCallback) => { + setError(undefined); + postGenerateLexeme({ + data: data, + showError: true, + setLoading, + onError: error => setError(error), + onSuccess: data => { + if (onSuccess) onSuccess(data); + } + }); + }, []); + + return { inflect, parse, generateLexeme, error, setError, loading }; +} + +export default useConceptText; diff --git a/rsconcept/frontend/src/hooks/useResolveText.ts b/rsconcept/frontend/src/hooks/useResolveText.ts index b1b42bc4..ab7a32ae 100644 --- a/rsconcept/frontend/src/hooks/useResolveText.ts +++ b/rsconcept/frontend/src/hooks/useResolveText.ts @@ -1,18 +1,18 @@ import { useCallback, useState } from 'react' import { ErrorInfo } from '../components/BackendError'; -import { IReferenceData } from '../models/language'; +import { IResolutionData } from '../models/language'; import { IRSForm } from '../models/rsform'; import { DataCallback, postResolveText } from '../utils/backendAPI'; function useResolveText({ schema }: { schema?: IRSForm }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(undefined); - const [refsData, setRefsData] = useState(undefined); + const [refsData, setRefsData] = useState(undefined); const resetData = useCallback(() => setRefsData(undefined), []); - function resolveText(text: string, onSuccess?: DataCallback) { + function resolveText(text: string, onSuccess?: DataCallback) { setError(undefined); postResolveText(String(schema!.id), { data: { text: text }, diff --git a/rsconcept/frontend/src/models/language.ts b/rsconcept/frontend/src/models/language.ts index 21856263..5a0b25f9 100644 --- a/rsconcept/frontend/src/models/language.ts +++ b/rsconcept/frontend/src/models/language.ts @@ -1,5 +1,12 @@ // Module: Natural language model declarations. +/** + * Represents API result for text output. +*/ +export interface ITextResult { + result: string +} + /** * Represents single unit of language Morphology. */ @@ -172,7 +179,6 @@ export const GrammemeGroups = [ */ export const NounGrams = [ Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS, - ...Gender, ...Case, ...Plurality ]; @@ -211,6 +217,21 @@ export interface IWordForm { grams: IGramData[] } +/** + * Represents wordform data used for backend communication. +*/ +export interface IWordFormPlain { + text: string + grams: string +} + +/** + * Represents lexeme response containing multiple {@link Wordform}s. +*/ +export interface ILexemeData { + items: IWordFormPlain[] +} + /** * Equality comparator for {@link IGramData}. Compares text data for unknown grammemes */ @@ -275,30 +296,48 @@ export function parseGrammemes(termForm: string): IGramData[] { } // ====== Reference resolution ===== -export interface IRefsText { +/** + * Represents text request. +*/ +export interface ITextRequest { text: string } +/** + * Represents text reference type. +*/ export enum ReferenceType { ENTITY = 'entity', SYNTACTIC = 'syntax' } +/** + * Represents entity reference payload. +*/ export interface IEntityReference { entity: string form: string } +/** + * Represents syntactic reference payload. +*/ export interface ISyntacticReference { offset: number nominal: string } +/** + * Represents text 0-indexed position inside another text. +*/ export interface ITextPosition { start: number finish: number } +/** + * Represents single resolved reference data. +*/ export interface IResolvedReference { type: ReferenceType data: IEntityReference | ISyntacticReference @@ -306,7 +345,10 @@ export interface IResolvedReference { pos_output: ITextPosition } -export interface IReferenceData { +/** + * Represents resolved references data for the whole text. +*/ +export interface IResolutionData { input: string output: string refs: IResolvedReference[] diff --git a/rsconcept/frontend/src/models/rsform.ts b/rsconcept/frontend/src/models/rsform.ts index f5f3a4ea..9c86ec56 100644 --- a/rsconcept/frontend/src/models/rsform.ts +++ b/rsconcept/frontend/src/models/rsform.ts @@ -81,7 +81,8 @@ export interface ICstMovetoData extends IConstituentaList { } export interface ICstUpdateData -extends Pick {} +extends Pick, +Partial> {} export interface ICstRenameData extends Pick {} diff --git a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx index c32b111c..2363c046 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/DlgEditTerm.tsx @@ -6,17 +6,19 @@ import Modal from '../../components/Common/Modal'; import SelectMulti from '../../components/Common/SelectMulti'; import TextArea from '../../components/Common/TextArea'; import DataTable, { createColumnHelper } from '../../components/DataTable'; -import { CheckIcon, ChevronDoubleUpIcon, ChevronUpIcon, CrossIcon } from '../../components/Icons'; +import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossIcon } from '../../components/Icons'; import { useConceptTheme } from '../../context/ThemeContext'; +import useConceptText from '../../hooks/useConceptText'; import { - Grammeme, GrammemeGroups, IWordForm, + Grammeme, GrammemeGroups, ITextRequest, IWordForm, + IWordFormPlain, matchWordForm, NounGrams, parseGrammemes, sortGrammemes, VerbGrams } from '../../models/language'; import { IConstituenta, TermForm } from '../../models/rsform'; import { colorfgGrammeme } from '../../utils/color'; import { labelGrammeme } from '../../utils/labels'; -import { IGrammemeOption, SelectorGrammems } from '../../utils/selectors'; +import { IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../../utils/selectors'; interface DlgEditTermProps { hideWindow: () => void @@ -27,6 +29,7 @@ interface DlgEditTermProps { const columnHelper = createColumnHelper(); function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { + const textProcessor = useConceptText(); const { colors } = useConceptTheme(); const [term, setTerm] = useState(''); @@ -41,7 +44,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { forms.forEach( ({text, grams}) => result.push({ text: text, - tags: grams.join(',') + tags: grams.map(gram => gram.data).join(',') })); return result; } @@ -58,6 +61,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { setForms(initForms); setTerm(target.term_resolved); setInputText(target.term_resolved); + setInputGrams([]); }, [target]); // Filter grammemes when input changes @@ -102,8 +106,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { })) }; setForms(forms => [ - ...forms.filter(value => !matchWordForm(value, newForm)), - newForm + newForm, + ...forms.filter(value => !matchWordForm(value, newForm)) ]); } @@ -121,22 +125,54 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { }); } + function handleRowClicked(form: IWordForm) { + setInputText(form.text); + setInputGrams(SelectorGrammems.filter(gram => form.grams.find(test => test.type === gram.type))); + } + function handleResetForm() { setInputText(''); setInputGrams([]); } - function handleGenerateSelected() { - + function handleInflect() { + const data: IWordFormPlain = { + text: term, + grams: inputGrams.map(gram => gram.data).join(',') + } + textProcessor.inflect(data, response => setInputText(response.result)); } - function handleGenerateBasics() { + function handleParse() { + const data: ITextRequest = { + text: inputText + } + textProcessor.parse(data, response => { + const grams = parseGrammemes(response.result); + setInputGrams(SelectorGrammems.filter(gram => grams.find(test => test.type === gram.type))); + }); + } + + function handleGenerateLexeme() { if (forms.length > 0) { if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) { return; } } - + const data: ITextRequest = { + text: inputText + } + textProcessor.generateLexeme(data, response => { + const newForms: IWordForm[] = response.items.map( + form => ({ + text: form.text, + grams: parseGrammemes(form.grams).filter(gram => SelectorGrammemesList.find(item => item === gram.type)) + })); + setForms(forms => [ + ...newForms, + ...forms.filter(value => !newForms.find(test => matchWordForm(value, test))), + ]); + }); } const columns = useMemo( @@ -156,10 +192,11 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { minSize: 250, maxSize: 250, cell: props => -
+
{ props.getValue().map( gram =>
setInputText(event.target.value)} /> -
- } - disabled={!inputText || inputGrams.length == 0} - onClick={handleAddForm} - /> - } - onClick={handleResetForm} - /> - } - disabled={inputGrams.length == 0} - onClick={handleGenerateSelected} - /> - } - onClick={handleGenerateBasics} - /> +
+
+ } + disabled={textProcessor.loading || !inputText || inputGrams.length == 0} + onClick={handleAddForm} + /> + } + disabled={textProcessor.loading} + onClick={handleResetForm} + /> + } + disabled={textProcessor.loading} + onClick={handleGenerateLexeme} + /> +
+
+ } + disabled={textProcessor.loading || inputGrams.length == 0} + onClick={handleInflect} + /> + } + disabled={textProcessor.loading || !inputText} + onClick={handleParse} + /> +
setInputGrams(sortGrammemes([...newValue]))} />
@@ -269,9 +320,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {

Добавьте словоформу

} - - // onRowDoubleClicked={handleDoubleClick} - // onRowClicked={handleRowClicked} + + onRowDoubleClicked={handleRowClicked} />
diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx index 0e7c1d4a..e6c3972a 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorConstituenta.tsx @@ -140,14 +140,15 @@ function EditorConstituenta({
+ {activeCst && } - /> + />}
Конституента diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx index 4c22c977..c61c3b20 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSTabs.tsx @@ -14,7 +14,7 @@ import { useConceptNavigation } from '../../context/NagivationContext'; import { useRSForm } from '../../context/RSFormContext'; import { useConceptTheme } from '../../context/ThemeContext'; import useModificationPrompt from '../../hooks/useModificationPrompt'; -import { ICstCreateData, ICstRenameData } from '../../models/rsform'; +import { ICstCreateData, ICstRenameData, ICstUpdateData, TermForm } from '../../models/rsform'; import { SyntaxTree } from '../../models/rslang'; import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; import { createAliasFor } from '../../utils/misc'; @@ -57,7 +57,7 @@ function RSTabs() { const search = useLocation().search; const { error, schema, loading, claim, download, isTracking, - cstCreate, cstDelete, cstRename, subscribe, unsubscribe + cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate } = useRSForm(); const { destroySchema } = useLibrary(); const { setNoFooter } = useConceptTheme(); @@ -304,6 +304,19 @@ function RSTabs() { setShowEditTerm(true); }, [isModified, activeCst]); + const handleSaveWordforms = useCallback( + (forms: TermForm[]) => { + if (!activeID) { + return; + } + const data: ICstUpdateData = { + id: activeID, + term_forms: forms + }; + console.log(data); + cstUpdate(data, () => toast.success('Изменения сохранены')); + }, [cstUpdate, activeID]); + return (
{ loading && } @@ -344,7 +357,7 @@ function RSTabs() { {showEditTerm && setShowEditTerm(false)} - onSave={() => {}} // TODO: implement cst update + onSave={handleSaveWordforms} target={activeCst!} />} ) { - AxiosPost({ - title: `Resolve text references for RSForm id=${schema}: ${request.data.text }`, - endpoint: `/api/rsforms/${schema}/resolve`, - request: request - }); -} - export function patchResetAliases(target: string, request: FrontPull) { AxiosPatch({ title: `Reset alias for RSForm id=${target}`, @@ -287,6 +278,38 @@ export function patchUploadTRS(target: string, request: FrontExchange) { + AxiosPost({ + title: `Resolve text references for RSForm id=${schema}: ${request.data.text}`, + endpoint: `/api/rsforms/${schema}/resolve`, + request: request + }); +} + +export function postInflectText(request: FrontExchange) { + AxiosPost({ + title: `Inflect text ${request.data.text} to ${request.data.grams}`, + endpoint: `/api/cctext/inflect`, + request: request + }); +} + +export function postParseText(request: FrontExchange) { + AxiosPost({ + title: `Parse text ${request.data.text}`, + endpoint: `/api/cctext/parse`, + request: request + }); +} + +export function postGenerateLexeme(request: FrontExchange) { + AxiosPost({ + title: `Parse text ${request.data.text}`, + endpoint: `/api/cctext/generate-lexeme`, + request: request + }); +} + // ============ Helper functions ============= function AxiosGet({ endpoint, request, title, options }: IAxiosRequest) { console.log(`REQUEST: [[${title}]]`); diff --git a/rsconcept/frontend/src/utils/color.ts b/rsconcept/frontend/src/utils/color.ts index 6643f1da..88b3118c 100644 --- a/rsconcept/frontend/src/utils/color.ts +++ b/rsconcept/frontend/src/utils/color.ts @@ -395,7 +395,10 @@ export function colorfgGrammeme(gram: Grammeme, colors: IColorTheme): string { if (VerbGrams.includes(gram)) { return colors.fgTeal; } - return colors.fgDefault; + if (gram === Grammeme.UNKN) { + return colors.fgRed; + } + return colors.fgPurple; } export function colorbgGrammeme(gram: Grammeme, colors: IColorTheme): string { diff --git a/rsconcept/frontend/src/utils/selectors.ts b/rsconcept/frontend/src/utils/selectors.ts index c8ba561c..f464f032 100644 --- a/rsconcept/frontend/src/utils/selectors.ts +++ b/rsconcept/frontend/src/utils/selectors.ts @@ -39,12 +39,11 @@ export const SelectorCstType = ( ); export interface IGrammemeOption extends IGramData { - value: Grammeme + value: string label: string } -export const SelectorGrammems: IGrammemeOption[] = -[ +export const SelectorGrammemesList = [ Grammeme.NOUN, Grammeme.VERB, Grammeme.sing, Grammeme.plur, @@ -61,10 +60,13 @@ export const SelectorGrammems: IGrammemeOption[] = Grammeme.impr, Grammeme.indc, Grammeme.incl, Grammeme.excl, Grammeme.pssv, Grammeme.actv, -].map( +]; + +export const SelectorGrammems: IGrammemeOption[] = +SelectorGrammemesList.map( gram => ({ type: gram, data: gram as string, - value: gram, + value: gram as string, label: labelGrammeme({type: gram, data: ''} as IGramData) }));