Implement termform editor

This commit is contained in:
IRBorisov 2023-09-25 14:17:52 +03:00
parent f7a7a1b173
commit 83242dfb69
23 changed files with 667 additions and 250 deletions

View File

@ -12,9 +12,9 @@ from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from apps.users.models import User 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 .graph import Graph
from .utils import apply_mapping_pattern from .utils import apply_pattern
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}') _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
@ -273,7 +273,14 @@ class RSForm:
''' Create resolver for text references based on schema terms. ''' ''' Create resolver for text references based on schema terms. '''
result = Resolver({}) result = Resolver({})
for cst in self.constituents(): 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 result.context[cst.alias] = entity
return result return result
@ -314,7 +321,10 @@ class RSForm:
raise ValidationError('Invalid position: should be positive integer') raise ValidationError('Invalid position: should be positive integer')
currentSize = self.constituents().count() currentSize = self.constituents().count()
position = max(1, min(position, currentSize + 1)) 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: for cst in update_list:
cst.order += 1 cst.order += 1
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])
@ -424,19 +434,19 @@ class RSForm:
if change_aliases and cst.alias in mapping: if change_aliases and cst.alias in mapping:
modified = True modified = True
cst.alias = mapping[cst.alias] 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: if expression != cst.definition_formal:
modified = True modified = True
cst.definition_formal = expression 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: if convention != cst.convention:
modified = True modified = True
cst.convention = convention 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: if term != cst.term_raw:
modified = True modified = True
cst.term_raw = term 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: if definition != cst.definition_raw:
modified = True modified = True
cst.definition_raw = definition cst.definition_raw = definition

View File

@ -148,18 +148,19 @@ class ConstituentaSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
def update(self, instance: Constituenta, validated_data) -> Constituenta: def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: create alias for better code readability
schema = RSForm(instance.schema) schema = RSForm(instance.schema)
definition: Optional[str] = validated_data['definition_raw'] if 'definition_raw' in validated_data else None definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None
term: Optional[str] = validated_data['term_raw'] if 'term_raw' in validated_data else None term: Optional[str] = data['term_raw'] if 'term_raw' in data else None
term_changed = False term_changed = 'term_forms' in data
if definition is not None and definition != instance.definition_raw : 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: if term is not None and term != instance.term_raw:
validated_data['term_resolved'] = schema.resolver().resolve(term) data['term_resolved'] = schema.resolver().resolve(term)
if validated_data['term_resolved'] != instance.term_resolved: if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data:
validated_data['term_forms'] = [] data['term_forms'] = []
term_changed = validated_data['term_resolved'] != instance.term_resolved term_changed = data['term_resolved'] != instance.term_resolved
result: Constituenta = super().update(instance, validated_data) result: Constituenta = super().update(instance, data)
if term_changed: if term_changed:
schema.on_term_change([result.alias]) schema.on_term_change([result.alias])
result.refresh_from_db() result.refresh_from_db()

View File

@ -2,7 +2,7 @@
import unittest import unittest
import re 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): class TestUtils(unittest.TestCase):
@ -10,10 +10,10 @@ class TestUtils(unittest.TestCase):
def test_apply_mapping_patter(self): def test_apply_mapping_patter(self):
mapping = {'X101': 'X20'} mapping = {'X101': 'X20'}
pattern = re.compile(r'(X[0-9]+)') pattern = re.compile(r'(X[0-9]+)')
self.assertEqual(apply_mapping_pattern('', mapping, pattern), '') self.assertEqual(apply_pattern('', mapping, pattern), '')
self.assertEqual(apply_mapping_pattern('X20', mapping, pattern), 'X20') self.assertEqual(apply_pattern('X20', mapping, pattern), 'X20')
self.assertEqual(apply_mapping_pattern('X101', mapping, pattern), 'X20') self.assertEqual(apply_pattern('X101', mapping, pattern), 'X20')
self.assertEqual(apply_mapping_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf') self.assertEqual(apply_pattern('asdf X101 asdf', mapping, pattern), 'asdf X20 asdf')
def test_fix_old_references(self): def test_fix_old_references(self):
self.assertEqual(fix_old_references(''), '') self.assertEqual(fix_old_references(''), '')

View File

@ -1,5 +1,4 @@
''' Testing views ''' ''' Testing views '''
import json
import os import os
import io import io
from zipfile import ZipFile from zipfile import ZipFile
@ -52,30 +51,43 @@ class TestConstituentaAPI(APITestCase):
self.assertEqual(response.data['convention'], self.cst1.convention) self.assertEqual(response.data['convention'], self.cst1.convention)
def test_partial_update(self): def test_partial_update(self):
data = json.dumps({'convention': 'tt'}) data = {'convention': 'tt'}
response = self.client.patch(f'/api/constituents/{self.cst2.id}', data, content_type='application/json') response = self.client.patch(
f'/api/constituents/{self.cst2.id}',
data=data, format='json'
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.client.logout() 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.assertEqual(response.status_code, 403)
self.client.force_authenticate(user=self.user) 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.assertEqual(response.status_code, 200)
self.cst1.refresh_from_db() self.cst1.refresh_from_db()
self.assertEqual(response.data['convention'], 'tt') self.assertEqual(response.data['convention'], 'tt')
self.assertEqual(self.cst1.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) self.assertEqual(response.status_code, 200)
def test_update_resolved_norefs(self): def test_update_resolved_norefs(self):
data = json.dumps({ data = {
'term_raw': 'New term', 'term_raw': 'New term',
'definition_raw': 'New def' '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.assertEqual(response.status_code, 200)
self.cst3.refresh_from_db() self.cst3.refresh_from_db()
self.assertEqual(response.data['term_resolved'], 'New term') self.assertEqual(response.data['term_resolved'], 'New term')
@ -84,11 +96,14 @@ class TestConstituentaAPI(APITestCase):
self.assertEqual(self.cst3.definition_resolved, 'New def') self.assertEqual(self.cst3.definition_resolved, 'New def')
def test_update_resolved_refs(self): def test_update_resolved_refs(self):
data = json.dumps({ data = {
'term_raw': '@{X1|nomn,sing}', 'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}' '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.assertEqual(response.status_code, 200)
self.cst3.refresh_from_db() self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved) 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') self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1')
def test_readonly_cst_fields(self): def test_readonly_cst_fields(self):
data = json.dumps({'alias': 'X33', 'order': 10}) data = {'alias': 'X33', 'order': 10}
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.assertEqual(response.status_code, 200)
self.assertEqual(response.data['alias'], 'X1') self.assertEqual(response.data['alias'], 'X1')
self.assertEqual(response.data['alias'], self.cst1.alias) self.assertEqual(response.data['alias'], self.cst1.alias)
@ -132,29 +150,33 @@ class TestLibraryViewset(APITestCase):
def test_create_anonymous(self): def test_create_anonymous(self):
self.client.logout() self.client.logout()
data = json.dumps({'title': 'Title'}) data = {'title': 'Title'}
response = self.client.post('/api/library', data=data, content_type='application/json') response = self.client.post('/api/library', data=data, format='json')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_create_populate_user(self): def test_create_populate_user(self):
data = json.dumps({'title': 'Title'}) data = {'title': 'Title'}
response = self.client.post('/api/library', data=data, content_type='application/json') response = self.client.post('/api/library', data=data, format='json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['title'], 'Title')
self.assertEqual(response.data['owner'], self.user.id) self.assertEqual(response.data['owner'], self.user.id)
def test_update(self): def test_update(self):
data = json.dumps({'id': self.owned.id, 'title': 'New title'}) data = {'id': self.owned.id, 'title': 'New title'}
response = self.client.patch(f'/api/library/{self.owned.id}', response = self.client.patch(
data=data, content_type='application/json') f'/api/library/{self.owned.id}',
data=data, format='json'
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['title'], 'New title') self.assertEqual(response.data['title'], 'New title')
self.assertEqual(response.data['alias'], self.owned.alias) self.assertEqual(response.data['alias'], self.owned.alias)
def test_update_unowned(self): def test_update_unowned(self):
data = json.dumps({'id': self.unowned.id, 'title': 'New title'}) data = {'id': self.unowned.id, 'title': 'New title'}
response = self.client.patch(f'/api/library/{self.unowned.id}', response = self.client.patch(
data=data, content_type='application/json') f'/api/library/{self.unowned.id}',
data=data, format='json'
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_destroy(self): def test_destroy(self):
@ -304,8 +326,11 @@ class TestRSFormViewset(APITestCase):
def test_check(self): def test_check(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_at(1, 'X1', CstType.BASE)
data = json.dumps({'expression': 'X1=X1'}) data = {'expression': 'X1=X1'}
response = self.client.post(f'/api/rsforms/{schema.item.id}/check', data=data, content_type='application/json') response = self.client.post(
f'/api/rsforms/{schema.item.id}/check',
data=data, format='json'
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], Syntax.MATH) self.assertEqual(response.data['syntax'], Syntax.MATH)
@ -318,8 +343,11 @@ class TestRSFormViewset(APITestCase):
x1 = schema.insert_at(1, 'X1', CstType.BASE) x1 = schema.insert_at(1, 'X1', CstType.BASE)
x1.term_resolved = 'синий слон' x1.term_resolved = 'синий слон'
x1.save() x1.save()
data = json.dumps({'text': '@{1|редкий} @{X1|plur,datv}'}) data = {'text': '@{1|редкий} @{X1|plur,datv}'}
response = self.client.post(f'/api/rsforms/{schema.item.id}/resolve', data=data, content_type='application/json') response = self.client.post(
f'/api/rsforms/{schema.item.id}/resolve',
data=data, format='json'
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}') self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам') self.assertEqual(response.data['output'], 'редким синим слонам')
@ -362,24 +390,30 @@ class TestRSFormViewset(APITestCase):
self.assertIn('document.json', zipped_file.namelist()) self.assertIn('document.json', zipped_file.namelist())
def test_create_constituenta(self): def test_create_constituenta(self):
data = json.dumps({'alias': 'X3', 'cst_type': 'basic'}) data = {'alias': 'X3', 'cst_type': 'basic'}
response = self.client.post(f'/api/rsforms/{self.unowned.item.id}/cst-create', response = self.client.post(
data=data, content_type='application/json') f'/api/rsforms/{self.unowned.item.id}/cst-create',
data=data, format='json'
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
item = self.owned.item item = self.owned.item
Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1) 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) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
response = self.client.post(f'/api/rsforms/{item.id}/cst-create', response = self.client.post(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-create',
data=data, format='json'
)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
data = json.dumps({'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id}) data = {'alias': 'X4', 'cst_type': 'basic', 'insert_after': x2.id}
response = self.client.post(f'/api/rsforms/{item.id}/cst-create', response = self.client.post(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-create',
data=data, format='json'
)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X4') self.assertEqual(response.data['new_cst']['alias'], 'X4')
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
@ -389,30 +423,39 @@ class TestRSFormViewset(APITestCase):
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.owned.item, order=1, convention='Test', alias='X1', schema=self.owned.item, order=1, convention='Test',
term_raw='Test1', term_resolved='Test1', 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( self.cst2 = Constituenta.objects.create(
alias='X2', schema=self.unowned.item, order=1, convention='Test1', 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( self.cst3 = Constituenta.objects.create(
alias='X3', schema=self.owned.item, order=2, alias='X3', schema=self.owned.item, order=2,
term_raw='Test3', term_resolved='Test3', 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}) data = {'alias': 'D2', 'cst_type': 'term', 'id': self.cst2.pk}
response = self.client.patch(f'/api/rsforms/{self.unowned.item.id}/cst-rename', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{self.unowned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
response = self.client.patch(f'/api/rsforms/{self.owned.item.id}/cst-rename', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = json.dumps({'alias': self.cst1.alias, 'cst_type': 'term', 'id': self.cst1.pk}) 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', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) 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 item = self.owned.item
d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4) d1 = Constituenta.objects.create(schema=item, alias='D1', cst_type='term', order=4)
d1.term_raw = '@{X1|plur}' d1.term_raw = '@{X1|plur}'
@ -423,8 +466,10 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(self.cst1.order, 1) self.assertEqual(self.cst1.order, 1)
self.assertEqual(self.cst1.alias, 'X1') self.assertEqual(self.cst1.alias, 'X1')
self.assertEqual(self.cst1.cst_type, CstType.BASE) self.assertEqual(self.cst1.cst_type, CstType.BASE)
response = self.client.patch(f'/api/rsforms/{item.id}/cst-rename', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-rename',
data=data, format='json'
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['new_cst']['alias'], 'D2') self.assertEqual(response.data['new_cst']['alias'], 'D2')
self.assertEqual(response.data['new_cst']['cst_type'], 'term') self.assertEqual(response.data['new_cst']['cst_type'], 'term')
@ -438,17 +483,19 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(self.cst1.cst_type, CstType.TERM) self.assertEqual(self.cst1.cst_type, CstType.TERM)
def test_create_constituenta_data(self): def test_create_constituenta_data(self):
data = json.dumps({ data = {
'alias': 'X3', 'alias': 'X3',
'cst_type': 'basic', 'cst_type': 'basic',
'convention': '1', 'convention': '1',
'term_raw': '2', 'term_raw': '2',
'definition_formal': '3', 'definition_formal': '3',
'definition_raw': '4' 'definition_raw': '4'
}) }
item = self.owned.item item = self.owned.item
response = self.client.post(f'/api/rsforms/{item.id}/cst-create', response = self.client.post(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-create',
data=data, format='json'
)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3') self.assertEqual(response.data['new_cst']['alias'], 'X3')
self.assertEqual(response.data['new_cst']['cst_type'], 'basic') self.assertEqual(response.data['new_cst']['cst_type'], 'basic')
@ -461,16 +508,20 @@ class TestRSFormViewset(APITestCase):
def test_delete_constituenta(self): def test_delete_constituenta(self):
schema = self.owned schema = self.owned
data = json.dumps({'items': [1337]}) data = {'items': [1337]}
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{schema.item.id}/cst-multidelete',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=schema.item, alias='X1', cst_type='basic', order=1) 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) x2 = Constituenta.objects.create(schema=schema.item, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [x1.id]}) data = {'items': [x1.id]}
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{schema.item.id}/cst-multidelete',
data=data, format='json'
)
x2.refresh_from_db() x2.refresh_from_db()
schema.item.refresh_from_db() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 202) self.assertEqual(response.status_code, 202)
@ -480,23 +531,29 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1) x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', order=1)
data = json.dumps({'items': [x3.id]}) data = {'items': [x3.id]}
response = self.client.patch(f'/api/rsforms/{schema.item.id}/cst-multidelete', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{schema.item.id}/cst-multidelete',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_move_constituenta(self): def test_move_constituenta(self):
item = self.owned.item item = self.owned.item
data = json.dumps({'items': [1337], 'move_to': 1}) data = {'items': [1337], 'move_to': 1}
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-moveto',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
x1 = Constituenta.objects.create(schema=item, alias='X1', cst_type='basic', order=1) 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) x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2)
data = json.dumps({'items': [x2.id], 'move_to': 1}) data = {'items': [x2.id], 'move_to': 1}
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-moveto',
data=data, format='json'
)
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -505,9 +562,11 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.unowned.item, alias='X1', cst_type='basic', 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}) data = {'items': [x3.id], 'move_to': 1}
response = self.client.patch(f'/api/rsforms/{item.id}/cst-moveto', response = self.client.patch(
data=data, content_type='application/json') f'/api/rsforms/{item.id}/cst-moveto',
data=data, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_reset_aliases(self): def test_reset_aliases(self):
@ -542,7 +601,10 @@ class TestRSFormViewset(APITestCase):
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False} 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() schema.item.refresh_from_db()
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(schema.item.title, 'Testt11') self.assertEqual(schema.item.title, 'Testt11')
@ -563,8 +625,11 @@ class TestRSFormViewset(APITestCase):
x1.save() x1.save()
d1.save() d1.save()
data = json.dumps({'title': 'Title'}) data = {'title': 'Title'}
response = self.client.post(f'/api/library/{item.id}/clone', data=data, content_type='application/json') response = self.client.post(
f'/api/library/{item.id}/clone',
data=data, format='json'
)
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['title'], 'Title') self.assertEqual(response.data['title'], 'Title')
@ -586,7 +651,10 @@ class TestRSLanguageViews(APITestCase):
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'title': 'Test123', 'comment': '123', 'alias': 'ks1'} 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.status_code, 201)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], 'Test123') self.assertEqual(response.data['title'], 'Test123')
@ -595,7 +663,10 @@ class TestRSLanguageViews(APITestCase):
def test_create_rsform_fallback(self): def test_create_rsform_fallback(self):
data = {'title': 'Test123', 'comment': '123', 'alias': 'ks1'} 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.status_code, 201)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], 'Test123') self.assertEqual(response.data['title'], 'Test123')
@ -604,35 +675,50 @@ class TestRSLanguageViews(APITestCase):
def test_convert_to_ascii(self): def test_convert_to_ascii(self):
data = {'expression': '1=1'} 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) response = convert_to_ascii(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['result'], r'1 \eq 1') self.assertEqual(response.data['result'], r'1 \eq 1')
def test_convert_to_ascii_missing_data(self): def test_convert_to_ascii_missing_data(self):
data = {'data': '1=1'} 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) response = convert_to_ascii(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
def test_convert_to_math(self): def test_convert_to_math(self):
data = {'expression': r'1 \eq 1'} 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) response = convert_to_math(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['result'], r'1=1') self.assertEqual(response.data['result'], r'1=1')
def test_convert_to_math_missing_data(self): def test_convert_to_math_missing_data(self):
data = {'data': r'1 \eq 1'} 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) response = convert_to_math(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
def test_parse_expression(self): def test_parse_expression(self):
data = {'expression': r'1=1'} 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) response = parse_expression(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
@ -641,7 +727,10 @@ class TestRSLanguageViews(APITestCase):
def test_parse_expression_missing_data(self): def test_parse_expression_missing_data(self):
data = {'data': r'1=1'} 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) response = parse_expression(request)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertIsInstance(response.data['expression'][0], ErrorDetail) self.assertIsInstance(response.data['expression'][0], ErrorDetail)
@ -657,21 +746,30 @@ class TestNaturalLanguageViews(APITestCase):
def test_parse_text(self): def test_parse_text(self):
data = {'text': 'синим слонам'} 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) response = parse_text(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc') self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc')
def test_inflect(self): def test_inflect(self):
data = {'text': 'синий слон', 'grams': 'plur,datv'} 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) response = inflect(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['result'], 'синим слонам') self.assertEqual(response.data['result'], 'синим слонам')
def test_generate_lexeme(self): def test_generate_lexeme(self):
data = {'text': 'синий слон'} 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) response = generate_lexeme(request)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data['items']), 12) self.assertEqual(len(response.data['items']), 12)

View File

@ -50,7 +50,7 @@ def write_trs(json_data: dict) -> bytes:
archive.writestr('document.json', data=data) archive.writestr('document.json', data=data)
return content.getvalue() 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. ''' ''' Apply mapping to matching in regular expression patter subgroup 1. '''
if text == '' or pattern == '': if text == '' or pattern == '':
return text return text

View File

@ -202,7 +202,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data 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() schema.item.refresh_from_db()
response = Response( response = Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
@ -251,7 +254,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def cst_multidelete(self, request, pk): def cst_multidelete(self, request, pk):
''' Endpoint: Delete multiple constituents. ''' ''' Endpoint: Delete multiple constituents. '''
schema = self._get_schema() 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) serializer.is_valid(raise_exception=True)
schema.delete_cst(serializer.validated_data['constituents']) schema.delete_cst(serializer.validated_data['constituents'])
schema.item.refresh_from_db() schema.item.refresh_from_db()
@ -270,9 +276,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def cst_moveto(self, request, pk): def cst_moveto(self, request, pk):
''' Endpoint: Move multiple constituents. ''' ''' Endpoint: Move multiple constituents. '''
schema = self._get_schema() 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) 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() schema.item.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, 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 = utils.read_trs(request.FILES['file'].file)
data['id'] = schema.item.pk 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) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
return Response( return Response(
@ -427,7 +442,10 @@ class TrsImportView(views.APIView):
if owner.is_anonymous: if owner.is_anonymous:
owner = None owner = None
_prepare_rsform_data(data, request, owner) _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) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
result = s.LibraryItemSerializer(schema.item) result = s.LibraryItemSerializer(schema.item)
@ -575,7 +593,7 @@ def inflect(request):
@extend_schema( @extend_schema(
summary='basic set of wordforms', summary='all wordforms for current lexeme',
tags=['NaturalLanguage'], tags=['NaturalLanguage'],
request=s.TextSerializer, request=s.TextSerializer,
responses={200: s.MultiFormSerializer}, responses={200: s.MultiFormSerializer},
@ -583,7 +601,7 @@ def inflect(request):
) )
@api_view(['POST']) @api_view(['POST'])
def generate_lexeme(request): 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 = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
nominal = serializer.validated_data['text'] nominal = serializer.validated_data['text']
@ -595,7 +613,7 @@ def generate_lexeme(request):
@extend_schema( @extend_schema(
summary='get all language parse variants', summary='get likely parse grammemes',
tags=['NaturalLanguage'], tags=['NaturalLanguage'],
request=s.TextSerializer, request=s.TextSerializer,
responses={200: s.ResultTextResponse}, responses={200: s.ResultTextResponse},

View File

@ -1,12 +1,10 @@
''' Testing views ''' ''' Testing views '''
import json
from rest_framework.test import APITestCase, APIClient from rest_framework.test import APITestCase, APIClient
from apps.users.models import User from apps.users.models import User
from apps.rsform.models import LibraryItem, LibraryItemType from apps.rsform.models import LibraryItem, LibraryItemType
# TODO: test ACTIVE_USERS
class TestUserAPIViews(APITestCase): class TestUserAPIViews(APITestCase):
def setUp(self): def setUp(self):
self.username = 'UserTest' self.username = 'UserTest'
@ -18,8 +16,11 @@ class TestUserAPIViews(APITestCase):
self.client = APIClient() self.client = APIClient()
def test_login(self): def test_login(self):
data = json.dumps({'username': self.username, 'password': self.password}) data = {'username': self.username, 'password': self.password}
response = self.client.post('/users/api/login', data=data, content_type='application/json') response = self.client.post(
'/users/api/login',
data=data, format='json'
)
self.assertEqual(response.status_code, 202) self.assertEqual(response.status_code, 202)
def test_logout(self): def test_logout(self):
@ -81,12 +82,15 @@ class TestUserUserProfileAPIView(APITestCase):
def test_patch_profile(self): def test_patch_profile(self):
self.client.force_login(user=self.user) self.client.force_login(user=self.user)
data = json.dumps({ data = {
'email': '123@mail.ru', 'email': '123@mail.ru',
'first_name': 'firstName', 'first_name': 'firstName',
'last_name': 'lastName', '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.status_code, 200)
self.assertEqual(response.data['email'], '123@mail.ru') self.assertEqual(response.data['email'], '123@mail.ru')
self.assertEqual(response.data['first_name'], 'firstName') self.assertEqual(response.data['first_name'], 'firstName')
@ -94,31 +98,52 @@ class TestUserUserProfileAPIView(APITestCase):
def test_edit_profile(self): def test_edit_profile(self):
newmail = 'newmail@gmail.com' newmail = 'newmail@gmail.com'
data = json.dumps({'email': newmail}) data = {'email': newmail}
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, 403) self.assertEqual(response.status_code, 403)
self.client.force_login(user=self.user) 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.status_code, 200)
self.assertEqual(response.data['username'], self.username) self.assertEqual(response.data['username'], self.username)
self.assertEqual(response.data['email'], newmail) self.assertEqual(response.data['email'], newmail)
def test_change_password(self): def test_change_password(self):
newpassword = 'pw2' newpassword = 'pw2'
data = json.dumps({'old_password': self.password, 'new_password': newpassword}) data = {
response = self.client.patch('/users/api/change-password', data, content_type='application/json') '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.assertEqual(response.status_code, 403)
self.assertFalse(self.client.login(username=self.user.username, password=newpassword)) self.assertFalse(self.client.login(username=self.user.username, password=newpassword))
self.assertTrue(self.client.login(username=self.user.username, password=self.password)) self.assertTrue(self.client.login(username=self.user.username, password=self.password))
invalid = json.dumps({'old_password': 'invalid', 'new_password': newpassword}) invalid = {
response = self.client.patch('/users/api/change-password', invalid, content_type='application/json') 'old_password': 'invalid',
'new_password': newpassword
}
response = self.client.patch(
'/users/api/change-password',
data=invalid, format='json'
)
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
oldHash = self.user.password oldHash = self.user.password
self.client.force_login(user=self.user) 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.user.refresh_from_db()
self.assertEqual(response.status_code, 204) self.assertEqual(response.status_code, 204)
self.assertNotEqual(self.user.password, oldHash) self.assertNotEqual(self.user.password, oldHash)
@ -131,15 +156,18 @@ class TestSignupAPIView(APITestCase):
self.client = APIClient() self.client = APIClient()
def test_signup(self): def test_signup(self):
data = json.dumps({ data = {
'username': 'TestUser', 'username': 'TestUser',
'email': 'email@mail.ru', 'email': 'email@mail.ru',
'password': 'Test@@123', 'password': 'Test@@123',
'password2': 'Test@@123', 'password2': 'Test@@123',
'first_name': 'firstName', 'first_name': 'firstName',
'last_name': 'lastName', '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.assertEqual(response.status_code, 201)
self.assertTrue('id' in response.data) self.assertTrue('id' in response.data)
self.assertEqual(response.data['username'], 'TestUser') self.assertEqual(response.data['username'], 'TestUser')

View File

@ -1,24 +1,40 @@
''' Term context for reference resolution. ''' ''' Term context for reference resolution. '''
from typing import Iterable, Dict, Optional, TypedDict from typing import Iterable, Dict, Optional, TypedDict
from .conceptapi import inflect from .ruparser import PhraseParser
from .rumodel import WordTag
parser = PhraseParser()
class TermForm(TypedDict): class TermForm(TypedDict):
''' Term in a specific form. ''' ''' Represents term in a specific form. '''
text: str text: str
tags: str grams: Iterable[str]
def _search_form(query: str, data: Iterable[TermForm]) -> Optional[str]: def _match_grams(query: Iterable[str], test: Iterable[str]) -> bool:
for tf in data: ''' Check if grams from test fit query. '''
if tf['tags'] == query: for gram in test:
return tf['text'] 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 return None
class Entity: class Entity:
''' Text entity. ''' ''' Represents text entity. '''
def __init__(self, alias: str, nominal: str, manual_forms: Optional[Iterable[TermForm]]=None): def __init__(self, alias: str, nominal: str, manual_forms: Optional[Iterable[TermForm]]=None):
if manual_forms is None: if manual_forms is None:
self.manual = [] self.manual = []
@ -41,20 +57,28 @@ class Entity:
self.manual = [] self.manual = []
self._cached = [] self._cached = []
def get_form(self, form: str) -> str: def get_form(self, grams: Iterable[str]) -> str:
''' Get specific term form. ''' ''' Get specific term form. '''
if form == '': if all(False for _ in grams):
return self._nominal return self._nominal
text = _search_form(form, self.manual) text = _search_form(grams, self.manual)
if text is None: if text is not None:
text = _search_form(form, self._cached) return text
if text is None: text = _search_form(grams, self._cached)
try: if text is not None:
text = inflect(self._nominal, form) return text
except ValueError as error:
text = f'!{error}!'.replace('Unknown grammeme', 'Неизвестная граммема') model = parser.parse(self._nominal)
self._cached.append({'text': text, 'tags': form}) 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 return text
# Term context for resolving entity references.
# Represents term context for resolving entity references.
TermContext = Dict[str, Entity] TermContext = Dict[str, Entity]

View File

@ -3,6 +3,8 @@ import re
from typing import cast, Optional from typing import cast, Optional
from dataclasses import dataclass from dataclasses import dataclass
from .rumodel import split_grams
from .conceptapi import inflect_dependant from .conceptapi import inflect_dependant
from .context import TermContext from .context import TermContext
from .reference import EntityReference, SyntacticReference, parse_reference, Reference from .reference import EntityReference, SyntacticReference, parse_reference, Reference
@ -24,7 +26,8 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str:
alias = ref.entity alias = ref.entity
if alias not in context: if alias not in context:
return f'!Неизвестная сущность: {alias}!' return f'!Неизвестная сущность: {alias}!'
resolved = context[alias].get_form(ref.form) grams = split_grams(ref.form)
resolved = context[alias].get_form(grams)
if resolved == '': if resolved == '':
return f'!Отсутствует термин: {alias}!' return f'!Отсутствует термин: {alias}!'
else: else:

View File

@ -9,24 +9,24 @@ class TestEntity(unittest.TestCase):
self.alias = 'X1' self.alias = 'X1'
self.nominal = 'человек' self.nominal = 'человек'
self.text1 = 'test1' self.text1 = 'test1'
self.form1 = 'sing,datv' self.form1 = ['sing','datv']
self.entity = Entity(self.alias, self.nominal, [{'text': self.text1, 'tags': self.form1}]) self.entity = Entity(self.alias, self.nominal, [{'text': self.text1, 'grams': self.form1}])
def test_attributes(self): def test_attributes(self):
self.assertEqual(self.entity.alias, self.alias) self.assertEqual(self.entity.alias, self.alias)
self.assertEqual(self.entity.get_nominal(), self.nominal) 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): 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(self.form1), self.text1)
self.assertEqual(self.entity.get_form('invalid tags'), '!Неизвестная граммема: invalid tags!') self.assertEqual(self.entity.get_form(['invalid tags']), '!Неизвестная граммема: invalid tags!')
self.assertEqual(self.entity.get_form('plur'), 'люди') self.assertEqual(self.entity.get_form(['plur']), 'люди')
def test_set_nominal(self): def test_set_nominal(self):
new_nomial = 'TEST' new_nomial = 'TEST'
self.assertEqual(self.entity.get_form('plur'), 'люди') self.assertEqual(self.entity.get_form(['plur']), 'люди')
self.entity.set_nominal(new_nomial) self.entity.set_nominal(new_nomial)
self.assertEqual(self.entity.get_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, []) self.assertEqual(self.entity.manual, [])

View File

@ -2,9 +2,11 @@
import unittest import unittest
from typing import cast from typing import cast
from django.test import tag
from cctext import ( from cctext import (
EntityReference, TermContext, Entity, SyntacticReference, EntityReference, TermContext, Entity, SyntacticReference,
Resolver, ResolvedReference, Position, Resolver, ResolvedReference, Position, TermForm,
resolve_entity, resolve_syntactic, extract_entities 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[1].pos_output, Position(9, 15))
self.assertEqual(self.resolver.refs[2].pos_input, Position(28, 38)) self.assertEqual(self.resolver.refs[2].pos_input, Position(28, 38))
self.assertEqual(self.resolver.refs[2].pos_output, Position(16, 20)) 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}'), 'человек')

View File

@ -150,7 +150,7 @@ export default function DataTable<TData extends RowData>({
style={conditionalRowStyles && getRowStyles(row)} style={conditionalRowStyles && getRowStyles(row)}
> >
{enableRowSelection && {enableRowSelection &&
<td className='pl-3 pr-1 border-y'> <td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
<SelectRow row={row} /> <SelectRow row={row} />
</td>} </td>}
{row.getVisibleCells().map( {row.getVisibleCells().map(

View File

@ -252,6 +252,22 @@ export function ArrowDownIcon(props: IconProps) {
); );
} }
export function ArrowLeftIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12.707 17.293L8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z' />
</IconSVG>
);
}
export function ArrowRightIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11.293 17.293l1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z' />
</IconSVG>
);
}
export function CloneIcon(props: IconProps) { export function CloneIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 512 512' {...props}> <IconSVG viewbox='0 0 512 512' {...props}>
@ -407,6 +423,15 @@ export function ChevronDoubleUpIcon(props: IconProps) {
); );
} }
export function ChevronDoubleDownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 15.586l-4.293-4.293-1.414 1.414L12 18.414l5.707-5.707-1.414-1.414z' />
<path d='M17.707 7.707l-1.414-1.414L12 10.586 7.707 6.293 6.293 7.707 12 13.414z' />
</IconSVG>
);
}
export function CheckIcon(props: IconProps) { export function CheckIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>

View File

@ -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<ErrorInfo>(undefined);
const inflect = useCallback(
(data: IWordFormPlain, onSuccess: DataCallback<ITextResult>) => {
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<ITextResult>) => {
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<ILexemeData>) => {
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;

View File

@ -1,18 +1,18 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { IReferenceData } from '../models/language'; import { IResolutionData } from '../models/language';
import { IRSForm } from '../models/rsform'; import { IRSForm } from '../models/rsform';
import { DataCallback, postResolveText } from '../utils/backendAPI'; import { DataCallback, postResolveText } from '../utils/backendAPI';
function useResolveText({ schema }: { schema?: IRSForm }) { function useResolveText({ schema }: { schema?: IRSForm }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined); const [refsData, setRefsData] = useState<IResolutionData | undefined>(undefined);
const resetData = useCallback(() => setRefsData(undefined), []); const resetData = useCallback(() => setRefsData(undefined), []);
function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) { function resolveText(text: string, onSuccess?: DataCallback<IResolutionData>) {
setError(undefined); setError(undefined);
postResolveText(String(schema!.id), { postResolveText(String(schema!.id), {
data: { text: text }, data: { text: text },

View File

@ -1,5 +1,12 @@
// Module: Natural language model declarations. // Module: Natural language model declarations.
/**
* Represents API result for text output.
*/
export interface ITextResult {
result: string
}
/** /**
* Represents single unit of language Morphology. * Represents single unit of language Morphology.
*/ */
@ -172,7 +179,6 @@ export const GrammemeGroups = [
*/ */
export const NounGrams = [ export const NounGrams = [
Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS, Grammeme.NOUN, Grammeme.ADJF, Grammeme.ADJS,
...Gender,
...Case, ...Case,
...Plurality ...Plurality
]; ];
@ -211,6 +217,21 @@ export interface IWordForm {
grams: IGramData[] 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 * Equality comparator for {@link IGramData}. Compares text data for unknown grammemes
*/ */
@ -275,30 +296,48 @@ export function parseGrammemes(termForm: string): IGramData[] {
} }
// ====== Reference resolution ===== // ====== Reference resolution =====
export interface IRefsText { /**
* Represents text request.
*/
export interface ITextRequest {
text: string text: string
} }
/**
* Represents text reference type.
*/
export enum ReferenceType { export enum ReferenceType {
ENTITY = 'entity', ENTITY = 'entity',
SYNTACTIC = 'syntax' SYNTACTIC = 'syntax'
} }
/**
* Represents entity reference payload.
*/
export interface IEntityReference { export interface IEntityReference {
entity: string entity: string
form: string form: string
} }
/**
* Represents syntactic reference payload.
*/
export interface ISyntacticReference { export interface ISyntacticReference {
offset: number offset: number
nominal: string nominal: string
} }
/**
* Represents text 0-indexed position inside another text.
*/
export interface ITextPosition { export interface ITextPosition {
start: number start: number
finish: number finish: number
} }
/**
* Represents single resolved reference data.
*/
export interface IResolvedReference { export interface IResolvedReference {
type: ReferenceType type: ReferenceType
data: IEntityReference | ISyntacticReference data: IEntityReference | ISyntacticReference
@ -306,7 +345,10 @@ export interface IResolvedReference {
pos_output: ITextPosition pos_output: ITextPosition
} }
export interface IReferenceData { /**
* Represents resolved references data for the whole text.
*/
export interface IResolutionData {
input: string input: string
output: string output: string
refs: IResolvedReference[] refs: IResolvedReference[]

View File

@ -81,7 +81,8 @@ export interface ICstMovetoData extends IConstituentaList {
} }
export interface ICstUpdateData export interface ICstUpdateData
extends Pick<IConstituentaMeta, 'id' | 'alias' | 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw'> {} extends Pick<IConstituentaMeta, 'id'>,
Partial<Pick<IConstituentaMeta, | 'alias' | 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'>> {}
export interface ICstRenameData export interface ICstRenameData
extends Pick<IConstituentaMeta, 'id' | 'alias' | 'cst_type' > {} extends Pick<IConstituentaMeta, 'id' | 'alias' | 'cst_type' > {}

View File

@ -6,17 +6,19 @@ import Modal from '../../components/Common/Modal';
import SelectMulti from '../../components/Common/SelectMulti'; import SelectMulti from '../../components/Common/SelectMulti';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import DataTable, { createColumnHelper } from '../../components/DataTable'; 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 { useConceptTheme } from '../../context/ThemeContext';
import useConceptText from '../../hooks/useConceptText';
import { import {
Grammeme, GrammemeGroups, IWordForm, Grammeme, GrammemeGroups, ITextRequest, IWordForm,
IWordFormPlain,
matchWordForm, NounGrams, parseGrammemes, matchWordForm, NounGrams, parseGrammemes,
sortGrammemes, VerbGrams sortGrammemes, VerbGrams
} from '../../models/language'; } from '../../models/language';
import { IConstituenta, TermForm } from '../../models/rsform'; import { IConstituenta, TermForm } from '../../models/rsform';
import { colorfgGrammeme } from '../../utils/color'; import { colorfgGrammeme } from '../../utils/color';
import { labelGrammeme } from '../../utils/labels'; import { labelGrammeme } from '../../utils/labels';
import { IGrammemeOption, SelectorGrammems } from '../../utils/selectors'; import { IGrammemeOption, SelectorGrammemesList, SelectorGrammems } from '../../utils/selectors';
interface DlgEditTermProps { interface DlgEditTermProps {
hideWindow: () => void hideWindow: () => void
@ -27,6 +29,7 @@ interface DlgEditTermProps {
const columnHelper = createColumnHelper<IWordForm>(); const columnHelper = createColumnHelper<IWordForm>();
function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) { function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
const textProcessor = useConceptText();
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [term, setTerm] = useState(''); const [term, setTerm] = useState('');
@ -41,7 +44,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
forms.forEach( forms.forEach(
({text, grams}) => result.push({ ({text, grams}) => result.push({
text: text, text: text,
tags: grams.join(',') tags: grams.map(gram => gram.data).join(',')
})); }));
return result; return result;
} }
@ -58,6 +61,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
setForms(initForms); setForms(initForms);
setTerm(target.term_resolved); setTerm(target.term_resolved);
setInputText(target.term_resolved); setInputText(target.term_resolved);
setInputGrams([]);
}, [target]); }, [target]);
// Filter grammemes when input changes // Filter grammemes when input changes
@ -102,8 +106,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
})) }))
}; };
setForms(forms => [ 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() { function handleResetForm() {
setInputText(''); setInputText('');
setInputGrams([]); 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 (forms.length > 0) {
if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) { if (!window.confirm('Данное действие приведет к перезаписи словоформ при совпадении граммем. Продолжить?')) {
return; 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( const columns = useMemo(
@ -156,10 +192,11 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
minSize: 250, minSize: 250,
maxSize: 250, maxSize: 250,
cell: props => cell: props =>
<div className='flex justify-start gap-1 select-none'> <div className='flex flex-wrap justify-start gap-1 select-none'>
{ props.getValue().map( { props.getValue().map(
gram => gram =>
<div <div
key={`${props.cell.id}-${gram.type}`}
className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap' className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
title='' title=''
style={{ style={{
@ -217,34 +254,47 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
<TextArea <TextArea
placeholder='Введите текст' placeholder='Введите текст'
rows={2} rows={2}
dimensions='min-w-[20rem]' dimensions='min-w-[20rem] min-h-[4.2rem]'
disabled={textProcessor.loading}
value={inputText} value={inputText}
onChange={event => setInputText(event.target.value)} onChange={event => setInputText(event.target.value)}
/> />
<div className='flex items-center justify-start'> <div className='flex items-center justify-between'>
<MiniButton <div className='flex items-center justify-start'>
tooltip='Добавить словоформу' <MiniButton
icon={<CheckIcon size={6} color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}/>} tooltip='Добавить словоформу'
disabled={!inputText || inputGrams.length == 0} icon={<CheckIcon size={6} color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}/>}
onClick={handleAddForm} disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
/> onClick={handleAddForm}
<MiniButton />
tooltip='Сбросить словоформу' <MiniButton
icon={<CrossIcon size={6} color='text-warning'/>} tooltip='Сбросить словоформу'
onClick={handleResetForm} icon={<CrossIcon size={6} color='text-warning'/>}
/> disabled={textProcessor.loading}
<MiniButton onClick={handleResetForm}
tooltip='Генерировать словоформу' />
icon={<ChevronUpIcon size={6} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'}/>} <MiniButton
disabled={inputGrams.length == 0} tooltip='Генерировать все словоформы'
onClick={handleGenerateSelected} icon={<ChevronDoubleDownIcon size={6} color='text-primary'/>}
/> disabled={textProcessor.loading}
<MiniButton onClick={handleGenerateLexeme}
tooltip='Генерировать базовые словоформы' />
icon={<ChevronDoubleUpIcon size={6} color='text-primary'/>} </div>
onClick={handleGenerateBasics} <div className='flex items-center justify-start'>
/> <MiniButton
tooltip='Генерировать словоформу'
icon={<ArrowLeftIcon size={6} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'}/>}
disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect}
/>
<MiniButton
tooltip='Определить граммемы'
icon={<ArrowRightIcon size={6} color={!inputText ? 'text-disabled' : 'text-primary'}/>}
disabled={textProcessor.loading || !inputText}
onClick={handleParse}
/>
</div>
</div> </div>
</div> </div>
<SelectMulti <SelectMulti
@ -253,6 +303,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
value={inputGrams} value={inputGrams}
isDisabled={textProcessor.loading}
onChange={newValue => setInputGrams(sortGrammemes([...newValue]))} onChange={newValue => setInputGrams(sortGrammemes([...newValue]))}
/> />
</div> </div>
@ -270,8 +321,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
</span> </span>
} }
// onRowDoubleClicked={handleDoubleClick} onRowDoubleClicked={handleRowClicked}
// onRowClicked={handleRowClicked}
/> />
</div> </div>
</div> </div>

View File

@ -140,14 +140,15 @@ function EditorConstituenta({
<form onSubmit={handleSubmit} className='min-w-[50rem] max-w-min px-4 py-2 border-y border-r'> <form onSubmit={handleSubmit} className='min-w-[50rem] max-w-min px-4 py-2 border-y border-r'>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 right-0 flex items-start justify-between w-full'> <div className='absolute top-0 right-0 flex items-start justify-between w-full'>
{activeCst &&
<MiniButton <MiniButton
tooltip='Редактировать словоформы термина' tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
disabled={!isEnabled} disabled={!isEnabled}
dimensions='w-fit pl-[3.2rem] pt-[0.4rem]' dimensions='w-fit ml-[3.2rem] pt-[0.4rem]'
noHover noHover
onClick={onEditTerm} onClick={onEditTerm}
icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />} icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
/> />}
<div className='flex items-center justify-center w-full gap-1'> <div className='flex items-center justify-center w-full gap-1'>
<div className='font-semibold w-fit'> <div className='font-semibold w-fit'>
<span className=''>Конституента </span> <span className=''>Конституента </span>

View File

@ -14,7 +14,7 @@ import { useConceptNavigation } from '../../context/NagivationContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useModificationPrompt from '../../hooks/useModificationPrompt'; 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 { SyntaxTree } from '../../models/rslang';
import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { createAliasFor } from '../../utils/misc'; import { createAliasFor } from '../../utils/misc';
@ -57,7 +57,7 @@ function RSTabs() {
const search = useLocation().search; const search = useLocation().search;
const { const {
error, schema, loading, claim, download, isTracking, error, schema, loading, claim, download, isTracking,
cstCreate, cstDelete, cstRename, subscribe, unsubscribe cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate
} = useRSForm(); } = useRSForm();
const { destroySchema } = useLibrary(); const { destroySchema } = useLibrary();
const { setNoFooter } = useConceptTheme(); const { setNoFooter } = useConceptTheme();
@ -304,6 +304,19 @@ function RSTabs() {
setShowEditTerm(true); setShowEditTerm(true);
}, [isModified, activeCst]); }, [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 ( return (
<div className='w-full'> <div className='w-full'>
{ loading && <ConceptLoader /> } { loading && <ConceptLoader /> }
@ -344,7 +357,7 @@ function RSTabs() {
{showEditTerm && {showEditTerm &&
<DlgEditTerm <DlgEditTerm
hideWindow={() => setShowEditTerm(false)} hideWindow={() => setShowEditTerm(false)}
onSave={() => {}} // TODO: implement cst update onSave={handleSaveWordforms}
target={activeCst!} target={activeCst!}
/>} />}
<Tabs <Tabs

View File

@ -1,24 +1,23 @@
import axios, { AxiosError, AxiosRequestConfig } from 'axios' import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify' import { toast } from 'react-toastify';
import { type ErrorInfo } from '../components/BackendError' import { type ErrorInfo } from '../components/BackendError';
import { IReferenceData } from '../models/language' import {
import { IRefsText } from '../models/language' ILexemeData,
import { ICurrentUser } from '../models/library' IResolutionData, ITextRequest,
import { IUserLoginData } from '../models/library' ITextResult, IWordFormPlain
import { IUserSignupData } from '../models/library' } from '../models/language';
import { IUserProfile } from '../models/library' import {
import { IUserUpdateData } from '../models/library' ICurrentUser, ILibraryItem, ILibraryUpdateData,
import { IUserInfo } from '../models/library' IUserInfo, IUserLoginData, IUserProfile, IUserSignupData,
import { IUserUpdatePassword } from '../models/library' IUserUpdateData, IUserUpdatePassword
import { ILibraryItem } from '../models/library' } from '../models/library';
import { ILibraryUpdateData } from '../models/library'
import { import {
IConstituentaList, IConstituentaMeta, IConstituentaList, IConstituentaMeta,
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData, ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData,
IRSFormCreateData, IRSFormData, IRSFormUploadData} from '../models/rsform' IRSFormCreateData, IRSFormData, IRSFormUploadData} from '../models/rsform';
import { IExpressionParse, IRSExpression } from '../models/rslang' import { IExpressionParse, IRSExpression } from '../models/rslang';
import { config } from './constants' import { config } from './constants';
export function initBackend() { export function initBackend() {
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
@ -258,14 +257,6 @@ export function postCheckExpression(schema: string, request: FrontExchange<IRSEx
}); });
} }
export function postResolveText(schema: string, request: FrontExchange<IRefsText, IReferenceData>) {
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<IRSFormData>) { export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({ AxiosPatch({
title: `Reset alias for RSForm id=${target}`, title: `Reset alias for RSForm id=${target}`,
@ -287,6 +278,38 @@ export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUpl
}); });
} }
export function postResolveText(schema: string, request: FrontExchange<ITextRequest, IResolutionData>) {
AxiosPost({
title: `Resolve text references for RSForm id=${schema}: ${request.data.text}`,
endpoint: `/api/rsforms/${schema}/resolve`,
request: request
});
}
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({
title: `Inflect text ${request.data.text} to ${request.data.grams}`,
endpoint: `/api/cctext/inflect`,
request: request
});
}
export function postParseText(request: FrontExchange<ITextRequest, ITextResult>) {
AxiosPost({
title: `Parse text ${request.data.text}`,
endpoint: `/api/cctext/parse`,
request: request
});
}
export function postGenerateLexeme(request: FrontExchange<ITextRequest, ILexemeData>) {
AxiosPost({
title: `Parse text ${request.data.text}`,
endpoint: `/api/cctext/generate-lexeme`,
request: request
});
}
// ============ Helper functions ============= // ============ Helper functions =============
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) { function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
console.log(`REQUEST: [[${title}]]`); console.log(`REQUEST: [[${title}]]`);

View File

@ -395,7 +395,10 @@ export function colorfgGrammeme(gram: Grammeme, colors: IColorTheme): string {
if (VerbGrams.includes(gram)) { if (VerbGrams.includes(gram)) {
return colors.fgTeal; return colors.fgTeal;
} }
return colors.fgDefault; if (gram === Grammeme.UNKN) {
return colors.fgRed;
}
return colors.fgPurple;
} }
export function colorbgGrammeme(gram: Grammeme, colors: IColorTheme): string { export function colorbgGrammeme(gram: Grammeme, colors: IColorTheme): string {

View File

@ -39,12 +39,11 @@ export const SelectorCstType = (
); );
export interface IGrammemeOption extends IGramData { export interface IGrammemeOption extends IGramData {
value: Grammeme value: string
label: string label: string
} }
export const SelectorGrammems: IGrammemeOption[] = export const SelectorGrammemesList = [
[
Grammeme.NOUN, Grammeme.VERB, Grammeme.NOUN, Grammeme.VERB,
Grammeme.sing, Grammeme.plur, Grammeme.sing, Grammeme.plur,
@ -61,10 +60,13 @@ export const SelectorGrammems: IGrammemeOption[] =
Grammeme.impr, Grammeme.indc, Grammeme.impr, Grammeme.indc,
Grammeme.incl, Grammeme.excl, Grammeme.incl, Grammeme.excl,
Grammeme.pssv, Grammeme.actv, Grammeme.pssv, Grammeme.actv,
].map( ];
export const SelectorGrammems: IGrammemeOption[] =
SelectorGrammemesList.map(
gram => ({ gram => ({
type: gram, type: gram,
data: gram as string, data: gram as string,
value: gram, value: gram as string,
label: labelGrammeme({type: gram, data: ''} as IGramData) label: labelGrammeme({type: gram, data: ''} as IGramData)
})); }));