mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add endpoint for constituenta substitution
This commit is contained in:
parent
dfdbd4b17c
commit
1aa80c3dce
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -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",
|
||||||
|
|
29
rsconcept/backend/apps/rsform/messages.py
Normal file
29
rsconcept/backend/apps/rsform/messages.py
Normal 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'
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
8
rsconcept/backend/apps/users/messages.py
Normal file
8
rsconcept/backend/apps/users/messages.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
''' Utility: Text messages. '''
|
||||||
|
# pylint: skip-file
|
||||||
|
|
||||||
|
def passwordAuthFailed():
|
||||||
|
return 'Неизвестное сочетание имени пользователя и пароля'
|
||||||
|
|
||||||
|
def passwordsNotMatch():
|
||||||
|
return 'Введенные пароли не совпадают'
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(''), '')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user