Add endpoint for constituenta substitution

This commit is contained in:
IRBorisov 2024-01-15 23:37:14 +03:00
parent dfdbd4b17c
commit 1aa80c3dce
14 changed files with 339 additions and 96 deletions

View File

@ -47,6 +47,7 @@
"clsx", "clsx",
"codemirror", "codemirror",
"Constituenta", "Constituenta",
"corsheaders",
"csrftoken", "csrftoken",
"cstlist", "cstlist",
"csttype", "csttype",
@ -67,6 +68,7 @@
"GRND", "GRND",
"impr", "impr",
"inan", "inan",
"incapsulation",
"indc", "indc",
"INFN", "INFN",
"Infr", "Infr",
@ -84,6 +86,7 @@
"NUMR", "NUMR",
"Opencorpora", "Opencorpora",
"perfectivity", "perfectivity",
"PNCT",
"ponomarev", "ponomarev",
"PRCL", "PRCL",
"PRTF", "PRTF",

View File

@ -0,0 +1,29 @@
''' Utility: Text messages. '''
# pylint: skip-file
def constituentaNotOwned(title: str):
return f'Конституента не принадлежит схеме: {title}'
def constituentaNotExists():
return 'Конституента не существует'
def renameTrivial(name: str):
return f'Имя должно отличаться от текущего: {name}'
def substituteTrivial(name: str):
return f'Отождествление конституенты с собой не корректно: {name}'
def renameTaken(name: str):
return f'Имя уже используется: {name}'
def pyconceptFailure():
return 'Invalid data response from pyconcept'
def libraryTypeUnexpected():
return 'Attempting to use invalid adaptor for non-RSForm item'
def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
def positionNegative():
return 'Invalid position: should be positive integer'

View File

@ -15,10 +15,11 @@ from apps.users.models import User
from cctext import Resolver, Entity, extract_entities, split_grams, TermForm from cctext import Resolver, Entity, extract_entities, split_grams, TermForm
from .graph import Graph from .graph import Graph
from .utils import apply_pattern from .utils import apply_pattern
from . import messages as msg
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}') _REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') _GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
class LibraryItemType(TextChoices): class LibraryItemType(TextChoices):
@ -125,7 +126,7 @@ class LibraryItem(Model):
def subscribers(self) -> list[User]: def subscribers(self) -> list[User]:
''' Get all subscribers for this item . ''' ''' Get all subscribers for this item . '''
return [s.user for s in Subscription.objects.filter(item=self.pk)] return [subscription.user for subscription in Subscription.objects.filter(item=self.pk)]
@transaction.atomic @transaction.atomic
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -272,7 +273,7 @@ class RSForm:
''' RSForm is a math form of capturing conceptual schema. ''' ''' RSForm is a math form of capturing conceptual schema. '''
def __init__(self, item: LibraryItem): def __init__(self, item: LibraryItem):
if item.item_type != LibraryItemType.RSFORM: if item.item_type != LibraryItemType.RSFORM:
raise ValueError('Attempting to use invalid adaptor for non-RSForm item') raise ValueError(msg.libraryTypeUnexpected())
self.item = item self.item = item
@staticmethod @staticmethod
@ -330,11 +331,12 @@ class RSForm:
@transaction.atomic @transaction.atomic
def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta': def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta':
''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' ''' Insert new constituenta at given position.
All following constituents order is shifted by 1 position. '''
if position <= 0: if position <= 0:
raise ValidationError('Invalid position: should be positive integer') raise ValidationError(msg.positionNegative())
if self.constituents().filter(alias=alias).exists(): if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}') raise ValidationError(msg.renameTaken(alias))
currentSize = self.constituents().count() currentSize = self.constituents().count()
position = max(1, min(position, currentSize + 1)) position = max(1, min(position, currentSize + 1))
update_list = \ update_list = \
@ -357,9 +359,9 @@ class RSForm:
@transaction.atomic @transaction.atomic
def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta':
''' Insert new constituenta at last position ''' ''' Insert new constituenta at last position. '''
if self.constituents().filter(alias=alias).exists(): if self.constituents().filter(alias=alias).exists():
raise ValidationError(f'Alias taken {alias}') raise ValidationError(msg.renameTaken(alias))
position = 1 position = 1
if self.constituents().exists(): if self.constituents().exists():
position += self.constituents().count() position += self.constituents().count()
@ -398,7 +400,7 @@ class RSForm:
@transaction.atomic @transaction.atomic
def delete_cst(self, listCst): def delete_cst(self, listCst):
''' Delete multiple constituents. Do not check if listCst are from this schema ''' ''' Delete multiple constituents. Do not check if listCst are from this schema. '''
for cst in listCst: for cst in listCst:
cst.delete() cst.delete()
self._reset_order() self._reset_order()
@ -426,8 +428,26 @@ class RSForm:
cst.refresh_from_db() cst.refresh_from_db()
return cst return cst
@transaction.atomic
def substitute(
self,
original: 'Constituenta',
substitution: 'Constituenta',
transfer_term: bool
):
''' Execute constituenta substitution. '''
assert original.pk != substitution.pk
mapping = { original.alias: substitution.alias }
self.apply_mapping(mapping)
if transfer_term:
substitution.term_raw = original.term_raw
substitution.term_forms = original.term_forms
substitution.save()
original.delete()
self.on_term_change([substitution.alias])
def reset_aliases(self): def reset_aliases(self):
''' Recreate all aliases based on cst order. ''' ''' Recreate all aliases based on constituents order. '''
mapping = self._create_reset_mapping() mapping = self._create_reset_mapping()
self.apply_mapping(mapping, change_aliases=True) self.apply_mapping(mapping, change_aliases=True)
@ -508,7 +528,10 @@ class RSForm:
def _term_graph(self) -> Graph: def _term_graph(self) -> Graph:
result = Graph() result = Graph()
cst_list = self.constituents().only('order', 'alias', 'term_raw').order_by('order') cst_list = \
self.constituents() \
.only('order', 'alias', 'term_raw') \
.order_by('order')
for cst in cst_list: for cst in cst_list:
result.add_node(cst.alias) result.add_node(cst.alias)
for cst in cst_list: for cst in cst_list:
@ -519,7 +542,10 @@ class RSForm:
def _definition_graph(self) -> Graph: def _definition_graph(self) -> Graph:
result = Graph() result = Graph()
cst_list = self.constituents().only('order', 'alias', 'definition_raw').order_by('order') cst_list = \
self.constituents() \
.only('order', 'alias', 'definition_raw') \
.order_by('order')
for cst in cst_list: for cst in cst_list:
result.add_node(cst.alias) result.add_node(cst.alias)
for cst in cst_list: for cst in cst_list:

View File

@ -9,6 +9,7 @@ from cctext import Resolver, Reference, ReferenceType, EntityReference, Syntacti
from .utils import fix_old_references from .utils import fix_old_references
from .models import Constituenta, LibraryItem, RSForm from .models import Constituenta, LibraryItem, RSForm
from . import messages as msg
_CST_TYPE = 'constituenta' _CST_TYPE = 'constituenta'
_TRS_TYPE = 'rsform' _TRS_TYPE = 'rsform'
@ -16,6 +17,8 @@ _TRS_VERSION_MIN = 16
_TRS_VERSION = 16 _TRS_VERSION = 16
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022' _TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
ConstituentaID = serializers.IntegerField
NodeID = serializers.IntegerField
class FileSerializer(serializers.Serializer): class FileSerializer(serializers.Serializer):
''' Serializer: File input. ''' ''' Serializer: File input. '''
@ -99,7 +102,7 @@ class NodeDataSerializer(serializers.Serializer):
class ASTNodeSerializer(serializers.Serializer): class ASTNodeSerializer(serializers.Serializer):
''' Serializer: Syntax tree node. ''' ''' Serializer: Syntax tree node. '''
uid = serializers.IntegerField() uid = NodeID()
parent = serializers.IntegerField() # type: ignore parent = serializers.IntegerField() # type: ignore
typeID = serializers.IntegerField() typeID = serializers.IntegerField()
start = serializers.IntegerField() start = serializers.IntegerField()
@ -148,7 +151,7 @@ class ConstituentaSerializer(serializers.ModelSerializer):
read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') 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 data = validated_data # Note: use alias for better code readability
schema = RSForm(instance.schema) schema = RSForm(instance.schema)
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None 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: Optional[str] = data['term_raw'] if 'term_raw' in data else None
@ -175,7 +178,10 @@ class CstCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = Constituenta model = Constituenta
fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after', 'term_forms' fields = \
'alias', 'cst_type', 'convention', \
'term_raw', 'definition_raw', 'definition_formal', \
'insert_after', 'term_forms'
class CstRenameSerializer(serializers.ModelSerializer): class CstRenameSerializer(serializers.ModelSerializer):
@ -191,15 +197,15 @@ class CstRenameSerializer(serializers.ModelSerializer):
new_alias = self.initial_data['alias'] new_alias = self.initial_data['alias']
if old_cst.schema != schema.item: if old_cst.schema != schema.item:
raise serializers.ValidationError({ raise serializers.ValidationError({
'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}' 'id': msg.constituentaNotOwned(schema.item.title)
}) })
if old_cst.alias == new_alias: if old_cst.alias == new_alias:
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': f'Имя конституенты должно отличаться от текущего: {new_alias}' 'alias': msg.renameTrivial(new_alias)
}) })
if schema.constituents().filter(alias=new_alias).exists(): if schema.constituents().filter(alias=new_alias).exists():
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': f'Конституента с таким именем уже существует: {new_alias}' 'alias': msg.renameTaken(new_alias)
}) })
self.instance = old_cst self.instance = old_cst
attrs['schema'] = schema.item attrs['schema'] = schema.item
@ -207,6 +213,34 @@ class CstRenameSerializer(serializers.ModelSerializer):
return attrs return attrs
class CstSubstituteSerializer(serializers.Serializer):
''' Serializer: Constituenta substitution. '''
original = ConstituentaID()
substitution = ConstituentaID()
transfer_term = serializers.BooleanField(required=False, default=False)
def validate(self, attrs):
schema = cast(RSForm, self.context['schema'])
original_cst = Constituenta.objects.get(pk=self.initial_data['original'])
substitution_cst = Constituenta.objects.get(pk=self.initial_data['substitution'])
if original_cst.alias == substitution_cst.alias:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
if original_cst.schema != schema.item:
raise serializers.ValidationError({
'original': msg.constituentaNotOwned(schema.item.title)
})
if substitution_cst.schema != schema.item:
raise serializers.ValidationError({
'substitution': msg.constituentaNotOwned(schema.item.title)
})
attrs['original'] = original_cst
attrs['substitution'] = substitution_cst
attrs['transfer_term'] = self.initial_data['transfer_term']
return attrs
class CstListSerializer(serializers.Serializer): class CstListSerializer(serializers.Serializer):
''' Serializer: List of constituents from one origin. ''' ''' Serializer: List of constituents from one origin. '''
items = serializers.ListField( items = serializers.ListField(
@ -220,12 +254,13 @@ class CstListSerializer(serializers.Serializer):
try: try:
cst = Constituenta.objects.get(pk=item) cst = Constituenta.objects.get(pk=item)
except Constituenta.DoesNotExist as exception: except Constituenta.DoesNotExist as exception:
raise serializers.ValidationError( raise serializers.ValidationError({
{f"{item}": 'Конституента не существует'} f'{item}': msg.constituentaNotExists
) from exception }) from exception
if cst.schema != schema.item: if cst.schema != schema.item:
raise serializers.ValidationError( raise serializers.ValidationError({
{'items': f'Конституенты должны относиться к данной схеме: {item}'}) f'{item}': msg.constituentaNotOwned(schema.item.title)
})
cstList.append(cst) cstList.append(cst)
attrs['constituents'] = cstList attrs['constituents'] = cstList
return attrs return attrs
@ -310,7 +345,7 @@ class PyConceptAdapter:
Warning! Does not include texts. ''' Warning! Does not include texts. '''
self._produce_response() self._produce_response()
if self._checked_data is None: if self._checked_data is None:
raise ValueError('Invalid data response from pyconcept') raise ValueError(msg.pyconceptFailure())
return self._checked_data return self._checked_data
def _prepare_request(self) -> dict: def _prepare_request(self) -> dict:
@ -481,7 +516,7 @@ class RSFormTRSSerializer(serializers.Serializer):
or self.initial_data['version'] < _TRS_VERSION_MIN \ or self.initial_data['version'] < _TRS_VERSION_MIN \
or self.initial_data['version'] > _TRS_VERSION: or self.initial_data['version'] > _TRS_VERSION:
raise serializers.ValidationError({ raise serializers.ValidationError({
'version': 'Некорректная версия файла Экстеор. Сохраните файл в новой версии' 'version': msg.exteorFileVersionNotSupported()
}) })
return attrs return attrs

View File

@ -242,19 +242,44 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.term_resolved, 'слон') self.assertEqual(cst2.term_resolved, 'слон')
self.assertEqual(cst2.definition_resolved, 'слонам слоны') self.assertEqual(cst2.definition_resolved, 'слонам слоны')
def test_delete_cst(self): def test_apply_mapping(self):
schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X11', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM)
d1.definition_formal = 'X1 = X11 = X2'
d1.definition_raw = '@{X11|sing}'
d1.convention = 'X1'
d1.term_raw = '@{X1|plur}'
d1.save()
schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'})
d1.refresh_from_db()
self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression')
self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition')
self.assertEqual(d1.convention, 'X3', msg='Map IDs in convention')
self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term')
self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping')
self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping')
def test_substitute(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_last('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_last('D1', CstType.TERM)
d2 = schema.insert_last('D2', CstType.TERM) d1.definition_formal = x1.alias
schema.delete_cst([x2, d1]) d1.save()
x1.refresh_from_db() x1.term_raw = 'Test'
d2.refresh_from_db() x1.save()
schema.item.refresh_from_db() x2.term_raw = 'Test2'
x2.save()
schema.substitute(x1, x2, True)
x2.refresh_from_db()
d1.refresh_from_db()
self.assertEqual(schema.constituents().count(), 2) self.assertEqual(schema.constituents().count(), 2)
self.assertEqual(x1.order, 1) self.assertEqual(x2.term_raw, 'Test')
self.assertEqual(d2.order, 2) self.assertEqual(d1.definition_formal, x2.alias)
def test_move_cst(self): def test_move_cst(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')

View File

@ -36,16 +36,30 @@ class TestConstituentaAPI(APITestCase):
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2') self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', schema=self.rsform_owned.item, order=1, convention='Test', alias='X1',
term_raw='Test1', term_resolved='Test1R', schema=self.rsform_owned.item,
order=1,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
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.rsform_unowned.item, order=1, convention='Test1', alias='X2',
term_raw='Test2', term_resolved='Test2R') schema=self.rsform_unowned.item,
order=1,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.cst3 = Constituenta.objects.create( self.cst3 = Constituenta.objects.create(
alias='X3', schema=self.rsform_owned.item, order=2, alias='X3',
term_raw='Test3', term_resolved='Test3', schema=self.rsform_owned.item,
definition_raw='Test1', definition_resolved='Test2') order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
def test_retrieve(self): def test_retrieve(self):
response = self.client.get(f'/api/constituents/{self.cst1.id}') response = self.client.get(f'/api/constituents/{self.cst1.id}')
@ -421,8 +435,18 @@ class TestRSFormViewset(APITestCase):
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(
x2 = Constituenta.objects.create(schema=item, alias='X2', cst_type='basic', order=2) 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( response = self.client.post(
f'/api/rsforms/{item.id}/cst-create', f'/api/rsforms/{item.id}/cst-create',
data=data, format='json' data=data, format='json'
@ -452,21 +476,29 @@ class TestRSFormViewset(APITestCase):
def test_rename_constituenta(self): def test_rename_constituenta(self):
cst1 = Constituenta.objects.create( cst1 = Constituenta.objects.create(
alias='X1', schema=self.owned.item, order=1, convention='Test', alias='X1',
term_raw='Test1', term_resolved='Test1', 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'}]
) )
cst2 = Constituenta.objects.create( cst2 = Constituenta.objects.create(
alias='X2', schema=self.unowned.item, order=1, convention='Test1', alias='X2',
term_raw='Test2', term_resolved='Test2' schema=self.unowned.item,
order=1
) )
cst3 = Constituenta.objects.create( cst3 = Constituenta.objects.create(
alias='X3', schema=self.owned.item, order=2, alias='X3',
term_raw='Test3', term_resolved='Test3', schema=self.owned.item, order=2,
definition_raw='Test1', definition_resolved='Test2' term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
) )
data = {'alias': 'D2', 'cst_type': 'term', 'id': cst2.pk} data = {'id': cst2.pk, 'alias': 'D2', 'cst_type': 'term'}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.unowned.item.id}/cst-rename', f'/api/rsforms/{self.unowned.item.id}/cst-rename',
data=data, format='json' data=data, format='json'
@ -479,14 +511,14 @@ class TestRSFormViewset(APITestCase):
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = {'alias': cst1.alias, 'cst_type': 'term', 'id': cst1.pk} data = {'id': cst1.pk, 'alias': cst1.alias, 'cst_type': 'term'}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename', f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json' data=data, format='json'
) )
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
data = {'alias': cst3.alias, 'id': cst1.pk} data = {'id': cst1.pk, 'alias': cst3.alias}
response = self.client.patch( response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-rename', f'/api/rsforms/{self.owned.item.id}/cst-rename',
data=data, format='json' data=data, format='json'
@ -520,6 +552,74 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(cst1.alias, 'D2') self.assertEqual(cst1.alias, 'D2')
self.assertEqual(cst1.cst_type, CstType.TERM) self.assertEqual(cst1.cst_type, CstType.TERM)
def test_substitute_constituenta(self):
x1 = Constituenta.objects.create(
alias='X1',
schema=self.owned.item,
order=1,
term_raw='Test1',
term_resolved='Test1',
term_forms=[{'text':'form1', 'tags':'sing,datv'}]
)
x2 = Constituenta.objects.create(
alias='X2',
schema=self.owned.item,
order=2,
term_raw='Test2'
)
unowned = Constituenta.objects.create(
alias='X2',
schema=self.unowned.item,
order=1
)
data = {'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.unowned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 403)
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 400)
d1 = Constituenta.objects.create(
alias='D1',
schema=self.owned.item,
order=3,
term_raw='@{X2|sing,datv}',
definition_formal='X1'
)
data = {'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}
response = self.client.patch(
f'/api/rsforms/{self.owned.item.id}/cst-substitute',
data=data, format='json'
)
self.assertEqual(response.status_code, 200)
d1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(x2.term_raw, 'Test1')
self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2')
def test_create_constituenta_data(self): def test_create_constituenta_data(self):
data = { data = {
'alias': 'X3', 'alias': 'X3',

View File

@ -195,7 +195,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def get_permissions(self): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple', if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple',
'reset_aliases', 'cst_rename']: 'reset_aliases', 'cst_rename', 'cst_substitute']:
permission_classes = [utils.ObjectOwnerOrAdmin] permission_classes = [utils.ObjectOwnerOrAdmin]
else: else:
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
@ -256,6 +256,30 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
} }
) )
@extend_schema(
summary='substitute constituenta',
tags=['Constituenta'],
request=s.CstSubstituteSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-substitute')
def cst_substitute(self, request, pk):
''' Substitute occurrences of constituenta with another one. '''
schema = self._get_schema()
serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': schema})
serializer.is_valid(raise_exception=True)
schema.substitute(
original=serializer.validated_data['original'],
substitution=serializer.validated_data['substitution'],
transfer_term=serializer.validated_data['transfer_term']
)
schema.item.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data
)
@extend_schema( @extend_schema(
summary='delete constituents', summary='delete constituents',
tags=['Constituenta'], tags=['Constituenta'],

View File

@ -0,0 +1,8 @@
''' Utility: Text messages. '''
# pylint: skip-file
def passwordAuthFailed():
return 'Неизвестное сочетание имени пользователя и пароля'
def passwordsNotMatch():
return 'Введенные пароли не совпадают'

View File

@ -5,6 +5,7 @@ from rest_framework import serializers
from apps.rsform.models import Subscription from apps.rsform.models import Subscription
from . import models from . import models
from . import messages as msg
class NonFieldErrorSerializer(serializers.Serializer): class NonFieldErrorSerializer(serializers.Serializer):
@ -36,17 +37,13 @@ class LoginSerializer(serializers.Serializer):
password=password password=password
) )
if not user: if not user:
msg = 'Неправильное сочетание имени пользователя и пароля.' raise serializers.ValidationError(
raise serializers.ValidationError(msg, code='authorization') msg.passwordAuthFailed(),
code='authorization'
)
attrs['user'] = user attrs['user'] = user
return attrs return attrs
def create(self, validated_data):
raise NotImplementedError('unexpected `create()` call')
def update(self, instance, validated_data):
raise NotImplementedError('unexpected `update()` call')
class AuthSerializer(serializers.Serializer): class AuthSerializer(serializers.Serializer):
''' Serializer: Authorization data. ''' ''' Serializer: Authorization data. '''
@ -108,12 +105,6 @@ class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True) old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True)
def create(self, validated_data):
raise NotImplementedError('unexpected `create()` call')
def update(self, instance, validated_data):
raise NotImplementedError('unexpected `update()` call')
class SignupSerializer(serializers.ModelSerializer): class SignupSerializer(serializers.ModelSerializer):
''' Serializer: Create user profile. ''' ''' Serializer: Create user profile. '''
@ -136,7 +127,9 @@ class SignupSerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
if attrs['password'] != attrs['password2']: if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Введенные пароли не совпадают"}) raise serializers.ValidationError({
'password': msg.passwordsNotMatch()
})
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):

View File

@ -1,7 +1,7 @@
''' '''
Concept API Python functions. Concept API Python functions.
::guarantee:: doesnt raise exceptions and returns workable outputs ::guarantee:: doesn't raise exceptions and returns workable outputs
''' '''
from cctext.rumodel import Morphology from cctext.rumodel import Morphology
from .syntax import RuSyntax from .syntax import RuSyntax
@ -56,9 +56,9 @@ def inflect(text: str, target_grams: str) -> str:
return model.inflect(target_set) return model.inflect(target_set)
def inflect_context(target: str, cntxt_before: str = '', cntxt_after: str = '') -> str: def inflect_context(target: str, before: str = '', after: str = '') -> str:
''' Inflect text in accordance to context before and after. ''' ''' Inflect text in accordance to context before and after. '''
return parser.inflect_context(target, cntxt_before, cntxt_after) return parser.inflect_context(target, before, after)
def inflect_substitute(substitute_normal: str, original: str) -> str: def inflect_substitute(substitute_normal: str, original: str) -> str:

View File

@ -25,11 +25,11 @@ class EntityReference:
class SyntacticReference: class SyntacticReference:
''' Reference to syntactic dependcy on EntityReference. ''' ''' Reference to syntactic dependency on EntityReference. '''
def __init__(self, referal_offset: int, text: str): def __init__(self, referral_offset: int, text: str):
self.nominal = text self.nominal = text
self.offset = referal_offset self.offset = referral_offset
def get_type(self) -> ReferenceType: def get_type(self) -> ReferenceType:
return ReferenceType.syntactic return ReferenceType.syntactic

View File

@ -34,31 +34,31 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str:
return resolved return resolved
def resolve_syntactic(ref: SyntacticReference, index: int, allrefs: list['ResolvedReference']) -> str: def resolve_syntactic(ref: SyntacticReference, index: int, references: list['ResolvedReference']) -> str:
''' Resolve syntactic reference. ''' ''' Resolve syntactic reference. '''
offset = ref.offset offset = ref.offset
mainref: Optional['ResolvedReference'] = None master: Optional['ResolvedReference'] = None
if offset > 0: if offset > 0:
index += 1 index += 1
while index < len(allrefs): while index < len(references):
if isinstance(allrefs[index].ref, EntityReference): if isinstance(references[index].ref, EntityReference):
if offset == 1: if offset == 1:
mainref = allrefs[index] master = references[index]
else: else:
offset -= 1 offset -= 1
index += 1 index += 1
else: else:
index -= 1 index -= 1
while index >= 0: while index >= 0:
if isinstance(allrefs[index].ref, EntityReference): if isinstance(references[index].ref, EntityReference):
if offset == -1: if offset == -1:
mainref = allrefs[index] master = references[index]
else: else:
offset += 1 offset += 1
index -= 1 index -= 1
if mainref is None: if master is None:
return f'!Некорректное смещение: {ref.offset}!' return f'!Некорректное смещение: {ref.offset}!'
return inflect_dependant(ref.nominal, mainref.resolved) return inflect_dependant(ref.nominal, master.resolved)
@dataclass @dataclass

View File

@ -149,7 +149,7 @@ class PhraseParser:
_SINGLE_SCORE_SEARCH = 0.2 _SINGLE_SCORE_SEARCH = 0.2
_PRIORITY_NONE = NO_COORDINATION _PRIORITY_NONE = NO_COORDINATION
_MAIN_WAIT_LIMIT = 10 # count words untill fixing main _MAIN_WAIT_LIMIT = 10 # count words until fixing main
_MAIN_MAX_FOLLOWERS = 3 # count words after main as coordination candidates _MAIN_MAX_FOLLOWERS = 3 # count words after main as coordination candidates
def parse(self, text: str, def parse(self, text: str,
@ -194,7 +194,7 @@ class PhraseParser:
return (start, token.stop) return (start, token.stop)
return (0, 0) return (0, 0)
def inflect_context(self, text: str, cntxt_before: str = '', cntxt_after: str = '') -> str: def inflect_context(self, text: str, before: str = '', after: str = '') -> str:
''' Inflect text in accordance to context before and after. ''' ''' Inflect text in accordance to context before and after. '''
target = self.parse(text) target = self.parse(text)
if not target: if not target:
@ -203,8 +203,8 @@ class PhraseParser:
if not target_morpho or not target_morpho.can_coordinate: if not target_morpho or not target_morpho.can_coordinate:
return text return text
model_after = self.parse(cntxt_after) model_after = self.parse(after)
model_before = self.parse(cntxt_before) model_before = self.parse(before)
etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after) etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after)
if not etalon: if not etalon:
return text return text

View File

@ -40,14 +40,14 @@ class TestResolver(unittest.TestCase):
def test_resolve_syntactic(self): def test_resolve_syntactic(self):
ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку') ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку')
allrefs = [ref, ref, ref, ref] allrefs = [ref, ref, ref, ref]
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 0, allrefs), '!Некорректное смещение: -1!') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 0, allrefs), '!Некорректное смещение: -1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 3, allrefs), '!Некорректное смещение: 1!') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 3, allrefs), '!Некорректное смещение: 1!')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 0, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=2), 0, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=2), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=3), 0, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=3), 0, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 3, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-2), 3, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-2), 3, allrefs), 'умному')
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-3), 3, allrefs), 'умному') self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-3), 3, allrefs), 'умному')
def test_resolve_invalid(self): def test_resolve_invalid(self):
self.assertEqual(self.resolver.resolve(''), '') self.assertEqual(self.resolver.resolve(''), '')