mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement termform editor
This commit is contained in:
parent
f7a7a1b173
commit
83242dfb69
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(''), '')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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, [])
|
||||
|
|
|
@ -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}'), 'человек')
|
||||
|
|
|
@ -150,7 +150,7 @@ export default function DataTable<TData extends RowData>({
|
|||
style={conditionalRowStyles && getRowStyles(row)}
|
||||
>
|
||||
{enableRowSelection &&
|
||||
<td className='pl-3 pr-1 border-y'>
|
||||
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
|
||||
<SelectRow row={row} />
|
||||
</td>}
|
||||
{row.getVisibleCells().map(
|
||||
|
|
|
@ -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) {
|
||||
return (
|
||||
<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) {
|
||||
return (
|
||||
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||
|
|
56
rsconcept/frontend/src/hooks/useConceptText.ts
Normal file
56
rsconcept/frontend/src/hooks/useConceptText.ts
Normal 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;
|
|
@ -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<ErrorInfo>(undefined);
|
||||
const [refsData, setRefsData] = useState<IReferenceData | undefined>(undefined);
|
||||
const [refsData, setRefsData] = useState<IResolutionData | undefined>(undefined);
|
||||
|
||||
const resetData = useCallback(() => setRefsData(undefined), []);
|
||||
|
||||
function resolveText(text: string, onSuccess?: DataCallback<IReferenceData>) {
|
||||
function resolveText(text: string, onSuccess?: DataCallback<IResolutionData>) {
|
||||
setError(undefined);
|
||||
postResolveText(String(schema!.id), {
|
||||
data: { text: text },
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -81,7 +81,8 @@ export interface ICstMovetoData extends IConstituentaList {
|
|||
}
|
||||
|
||||
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
|
||||
extends Pick<IConstituentaMeta, 'id' | 'alias' | 'cst_type' > {}
|
||||
|
|
|
@ -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<IWordForm>();
|
||||
|
||||
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 =>
|
||||
<div className='flex justify-start gap-1 select-none'>
|
||||
<div className='flex flex-wrap justify-start gap-1 select-none'>
|
||||
{ props.getValue().map(
|
||||
gram =>
|
||||
<div
|
||||
key={`${props.cell.id}-${gram.type}`}
|
||||
className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
|
||||
title=''
|
||||
style={{
|
||||
|
@ -217,34 +254,47 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
|
|||
<TextArea
|
||||
placeholder='Введите текст'
|
||||
rows={2}
|
||||
dimensions='min-w-[20rem]'
|
||||
|
||||
dimensions='min-w-[20rem] min-h-[4.2rem]'
|
||||
|
||||
disabled={textProcessor.loading}
|
||||
value={inputText}
|
||||
onChange={event => setInputText(event.target.value)}
|
||||
/>
|
||||
<div className='flex items-center justify-start'>
|
||||
<MiniButton
|
||||
tooltip='Добавить словоформу'
|
||||
icon={<CheckIcon size={6} color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}/>}
|
||||
disabled={!inputText || inputGrams.length == 0}
|
||||
onClick={handleAddForm}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Сбросить словоформу'
|
||||
icon={<CrossIcon size={6} color='text-warning'/>}
|
||||
onClick={handleResetForm}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Генерировать словоформу'
|
||||
icon={<ChevronUpIcon size={6} color={inputGrams.length == 0 ? 'text-disabled' : 'text-primary'}/>}
|
||||
disabled={inputGrams.length == 0}
|
||||
onClick={handleGenerateSelected}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Генерировать базовые словоформы'
|
||||
icon={<ChevronDoubleUpIcon size={6} color='text-primary'/>}
|
||||
onClick={handleGenerateBasics}
|
||||
/>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center justify-start'>
|
||||
<MiniButton
|
||||
tooltip='Добавить словоформу'
|
||||
icon={<CheckIcon size={6} color={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'text-success'}/>}
|
||||
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
|
||||
onClick={handleAddForm}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Сбросить словоформу'
|
||||
icon={<CrossIcon size={6} color='text-warning'/>}
|
||||
disabled={textProcessor.loading}
|
||||
onClick={handleResetForm}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Генерировать все словоформы'
|
||||
icon={<ChevronDoubleDownIcon size={6} color='text-primary'/>}
|
||||
disabled={textProcessor.loading}
|
||||
onClick={handleGenerateLexeme}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<SelectMulti
|
||||
|
@ -253,6 +303,7 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
|
|||
placeholder='Выберите граммемы'
|
||||
|
||||
value={inputGrams}
|
||||
isDisabled={textProcessor.loading}
|
||||
onChange={newValue => setInputGrams(sortGrammemes([...newValue]))}
|
||||
/>
|
||||
</div>
|
||||
|
@ -269,9 +320,8 @@ function DlgEditTerm({ hideWindow, target, onSave }: DlgEditTermProps) {
|
|||
<p>Добавьте словоформу</p>
|
||||
</span>
|
||||
}
|
||||
|
||||
// onRowDoubleClicked={handleDoubleClick}
|
||||
// onRowClicked={handleRowClicked}
|
||||
|
||||
onRowDoubleClicked={handleRowClicked}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -140,14 +140,15 @@ function EditorConstituenta({
|
|||
<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='absolute top-0 right-0 flex items-start justify-between w-full'>
|
||||
{activeCst &&
|
||||
<MiniButton
|
||||
tooltip='Редактировать словоформы термина'
|
||||
tooltip={`Редактировать словоформы термина: ${activeCst.term_forms.length}`}
|
||||
disabled={!isEnabled}
|
||||
dimensions='w-fit pl-[3.2rem] pt-[0.4rem]'
|
||||
dimensions='w-fit ml-[3.2rem] pt-[0.4rem]'
|
||||
noHover
|
||||
onClick={onEditTerm}
|
||||
icon={<PenIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
|
||||
/>
|
||||
/>}
|
||||
<div className='flex items-center justify-center w-full gap-1'>
|
||||
<div className='font-semibold w-fit'>
|
||||
<span className=''>Конституента </span>
|
||||
|
|
|
@ -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 (
|
||||
<div className='w-full'>
|
||||
{ loading && <ConceptLoader /> }
|
||||
|
@ -344,7 +357,7 @@ function RSTabs() {
|
|||
{showEditTerm &&
|
||||
<DlgEditTerm
|
||||
hideWindow={() => setShowEditTerm(false)}
|
||||
onSave={() => {}} // TODO: implement cst update
|
||||
onSave={handleSaveWordforms}
|
||||
target={activeCst!}
|
||||
/>}
|
||||
<Tabs
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import axios, { AxiosError, AxiosRequestConfig } from 'axios'
|
||||
import { toast } from 'react-toastify'
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { type ErrorInfo } from '../components/BackendError'
|
||||
import { IReferenceData } from '../models/language'
|
||||
import { IRefsText } from '../models/language'
|
||||
import { ICurrentUser } from '../models/library'
|
||||
import { IUserLoginData } from '../models/library'
|
||||
import { IUserSignupData } from '../models/library'
|
||||
import { IUserProfile } from '../models/library'
|
||||
import { IUserUpdateData } from '../models/library'
|
||||
import { IUserInfo } from '../models/library'
|
||||
import { IUserUpdatePassword } from '../models/library'
|
||||
import { ILibraryItem } from '../models/library'
|
||||
import { ILibraryUpdateData } from '../models/library'
|
||||
import { type ErrorInfo } from '../components/BackendError';
|
||||
import {
|
||||
ILexemeData,
|
||||
IResolutionData, ITextRequest,
|
||||
ITextResult, IWordFormPlain
|
||||
} from '../models/language';
|
||||
import {
|
||||
ICurrentUser, ILibraryItem, ILibraryUpdateData,
|
||||
IUserInfo, IUserLoginData, IUserProfile, IUserSignupData,
|
||||
IUserUpdateData, IUserUpdatePassword
|
||||
} from '../models/library';
|
||||
import {
|
||||
IConstituentaList, IConstituentaMeta,
|
||||
ICstCreateData, ICstCreatedResponse, ICstMovetoData, ICstRenameData, ICstUpdateData,
|
||||
IRSFormCreateData, IRSFormData, IRSFormUploadData} from '../models/rsform'
|
||||
import { IExpressionParse, IRSExpression } from '../models/rslang'
|
||||
import { config } from './constants'
|
||||
IRSFormCreateData, IRSFormData, IRSFormUploadData} from '../models/rsform';
|
||||
import { IExpressionParse, IRSExpression } from '../models/rslang';
|
||||
import { config } from './constants';
|
||||
|
||||
export function initBackend() {
|
||||
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>) {
|
||||
AxiosPatch({
|
||||
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 =============
|
||||
function AxiosGet<ResponseData>({ endpoint, request, title, options }: IAxiosRequest<undefined, ResponseData>) {
|
||||
console.log(`REQUEST: [[${title}]]`);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}));
|
||||
|
|
Loading…
Reference in New Issue
Block a user