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",
|
||||
"codemirror",
|
||||
"Constituenta",
|
||||
"corsheaders",
|
||||
"csrftoken",
|
||||
"cstlist",
|
||||
"csttype",
|
||||
|
@ -67,6 +68,7 @@
|
|||
"GRND",
|
||||
"impr",
|
||||
"inan",
|
||||
"incapsulation",
|
||||
"indc",
|
||||
"INFN",
|
||||
"Infr",
|
||||
|
@ -84,6 +86,7 @@
|
|||
"NUMR",
|
||||
"Opencorpora",
|
||||
"perfectivity",
|
||||
"PNCT",
|
||||
"ponomarev",
|
||||
"PRCL",
|
||||
"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 .graph import Graph
|
||||
from .utils import apply_pattern
|
||||
from . import messages as msg
|
||||
|
||||
|
||||
_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):
|
||||
|
@ -125,7 +126,7 @@ class LibraryItem(Model):
|
|||
|
||||
def subscribers(self) -> list[User]:
|
||||
''' 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
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -272,7 +273,7 @@ class RSForm:
|
|||
''' RSForm is a math form of capturing conceptual schema. '''
|
||||
def __init__(self, item: LibraryItem):
|
||||
if item.item_type != LibraryItemType.RSFORM:
|
||||
raise ValueError('Attempting to use invalid adaptor for non-RSForm item')
|
||||
raise ValueError(msg.libraryTypeUnexpected())
|
||||
self.item = item
|
||||
|
||||
@staticmethod
|
||||
|
@ -330,11 +331,12 @@ class RSForm:
|
|||
|
||||
@transaction.atomic
|
||||
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:
|
||||
raise ValidationError('Invalid position: should be positive integer')
|
||||
raise ValidationError(msg.positionNegative())
|
||||
if self.constituents().filter(alias=alias).exists():
|
||||
raise ValidationError(f'Alias taken {alias}')
|
||||
raise ValidationError(msg.renameTaken(alias))
|
||||
currentSize = self.constituents().count()
|
||||
position = max(1, min(position, currentSize + 1))
|
||||
update_list = \
|
||||
|
@ -357,9 +359,9 @@ class RSForm:
|
|||
|
||||
@transaction.atomic
|
||||
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():
|
||||
raise ValidationError(f'Alias taken {alias}')
|
||||
raise ValidationError(msg.renameTaken(alias))
|
||||
position = 1
|
||||
if self.constituents().exists():
|
||||
position += self.constituents().count()
|
||||
|
@ -398,7 +400,7 @@ class RSForm:
|
|||
|
||||
@transaction.atomic
|
||||
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:
|
||||
cst.delete()
|
||||
self._reset_order()
|
||||
|
@ -426,8 +428,26 @@ class RSForm:
|
|||
cst.refresh_from_db()
|
||||
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):
|
||||
''' Recreate all aliases based on cst order. '''
|
||||
''' Recreate all aliases based on constituents order. '''
|
||||
mapping = self._create_reset_mapping()
|
||||
self.apply_mapping(mapping, change_aliases=True)
|
||||
|
||||
|
@ -508,7 +528,10 @@ class RSForm:
|
|||
|
||||
def _term_graph(self) -> 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:
|
||||
result.add_node(cst.alias)
|
||||
for cst in cst_list:
|
||||
|
@ -519,7 +542,10 @@ class RSForm:
|
|||
|
||||
def _definition_graph(self) -> 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:
|
||||
result.add_node(cst.alias)
|
||||
for cst in cst_list:
|
||||
|
|
|
@ -9,6 +9,7 @@ from cctext import Resolver, Reference, ReferenceType, EntityReference, Syntacti
|
|||
|
||||
from .utils import fix_old_references
|
||||
from .models import Constituenta, LibraryItem, RSForm
|
||||
from . import messages as msg
|
||||
|
||||
_CST_TYPE = 'constituenta'
|
||||
_TRS_TYPE = 'rsform'
|
||||
|
@ -16,6 +17,8 @@ _TRS_VERSION_MIN = 16
|
|||
_TRS_VERSION = 16
|
||||
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
|
||||
|
||||
ConstituentaID = serializers.IntegerField
|
||||
NodeID = serializers.IntegerField
|
||||
|
||||
class FileSerializer(serializers.Serializer):
|
||||
''' Serializer: File input. '''
|
||||
|
@ -99,7 +102,7 @@ class NodeDataSerializer(serializers.Serializer):
|
|||
|
||||
class ASTNodeSerializer(serializers.Serializer):
|
||||
''' Serializer: Syntax tree node. '''
|
||||
uid = serializers.IntegerField()
|
||||
uid = NodeID()
|
||||
parent = serializers.IntegerField() # type: ignore
|
||||
typeID = 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')
|
||||
|
||||
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)
|
||||
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
|
||||
|
@ -175,7 +178,10 @@ class CstCreateSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
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):
|
||||
|
@ -191,15 +197,15 @@ class CstRenameSerializer(serializers.ModelSerializer):
|
|||
new_alias = self.initial_data['alias']
|
||||
if old_cst.schema != schema.item:
|
||||
raise serializers.ValidationError({
|
||||
'id': f'Изменяемая конституента должна относиться к изменяемой схеме: {schema.item.title}'
|
||||
'id': msg.constituentaNotOwned(schema.item.title)
|
||||
})
|
||||
if old_cst.alias == new_alias:
|
||||
raise serializers.ValidationError({
|
||||
'alias': f'Имя конституенты должно отличаться от текущего: {new_alias}'
|
||||
'alias': msg.renameTrivial(new_alias)
|
||||
})
|
||||
if schema.constituents().filter(alias=new_alias).exists():
|
||||
raise serializers.ValidationError({
|
||||
'alias': f'Конституента с таким именем уже существует: {new_alias}'
|
||||
'alias': msg.renameTaken(new_alias)
|
||||
})
|
||||
self.instance = old_cst
|
||||
attrs['schema'] = schema.item
|
||||
|
@ -207,6 +213,34 @@ class CstRenameSerializer(serializers.ModelSerializer):
|
|||
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):
|
||||
''' Serializer: List of constituents from one origin. '''
|
||||
items = serializers.ListField(
|
||||
|
@ -220,12 +254,13 @@ class CstListSerializer(serializers.Serializer):
|
|||
try:
|
||||
cst = Constituenta.objects.get(pk=item)
|
||||
except Constituenta.DoesNotExist as exception:
|
||||
raise serializers.ValidationError(
|
||||
{f"{item}": 'Конституента не существует'}
|
||||
) from exception
|
||||
raise serializers.ValidationError({
|
||||
f'{item}': msg.constituentaNotExists
|
||||
}) from exception
|
||||
if cst.schema != schema.item:
|
||||
raise serializers.ValidationError(
|
||||
{'items': f'Конституенты должны относиться к данной схеме: {item}'})
|
||||
raise serializers.ValidationError({
|
||||
f'{item}': msg.constituentaNotOwned(schema.item.title)
|
||||
})
|
||||
cstList.append(cst)
|
||||
attrs['constituents'] = cstList
|
||||
return attrs
|
||||
|
@ -310,7 +345,7 @@ class PyConceptAdapter:
|
|||
Warning! Does not include texts. '''
|
||||
self._produce_response()
|
||||
if self._checked_data is None:
|
||||
raise ValueError('Invalid data response from pyconcept')
|
||||
raise ValueError(msg.pyconceptFailure())
|
||||
return self._checked_data
|
||||
|
||||
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:
|
||||
raise serializers.ValidationError({
|
||||
'version': 'Некорректная версия файла Экстеор. Сохраните файл в новой версии'
|
||||
'version': msg.exteorFileVersionNotSupported()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
|
|
@ -242,19 +242,44 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(cst2.term_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')
|
||||
x1 = schema.insert_last('X1', CstType.BASE)
|
||||
x2 = schema.insert_last('X2', CstType.BASE)
|
||||
d1 = schema.insert_last('D1', CstType.TERM)
|
||||
d2 = schema.insert_last('D2', CstType.TERM)
|
||||
schema.delete_cst([x2, d1])
|
||||
x1.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
schema.item.refresh_from_db()
|
||||
d1.definition_formal = x1.alias
|
||||
d1.save()
|
||||
x1.term_raw = 'Test'
|
||||
x1.save()
|
||||
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(x1.order, 1)
|
||||
self.assertEqual(d2.order, 2)
|
||||
self.assertEqual(x2.term_raw, 'Test')
|
||||
self.assertEqual(d1.definition_formal, x2.alias)
|
||||
|
||||
def test_move_cst(self):
|
||||
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_unowned = RSForm.create(title='Test2', alias='T2')
|
||||
self.cst1 = Constituenta.objects.create(
|
||||
alias='X1', schema=self.rsform_owned.item, order=1, convention='Test',
|
||||
term_raw='Test1', term_resolved='Test1R',
|
||||
alias='X1',
|
||||
schema=self.rsform_owned.item,
|
||||
order=1,
|
||||
convention='Test',
|
||||
term_raw='Test1',
|
||||
term_resolved='Test1R',
|
||||
term_forms=[{'text':'form1', 'tags':'sing,datv'}])
|
||||
self.cst2 = Constituenta.objects.create(
|
||||
alias='X2', schema=self.rsform_unowned.item, order=1, convention='Test1',
|
||||
term_raw='Test2', term_resolved='Test2R')
|
||||
alias='X2',
|
||||
schema=self.rsform_unowned.item,
|
||||
order=1,
|
||||
convention='Test1',
|
||||
term_raw='Test2',
|
||||
term_resolved='Test2R'
|
||||
)
|
||||
self.cst3 = Constituenta.objects.create(
|
||||
alias='X3', schema=self.rsform_owned.item, order=2,
|
||||
term_raw='Test3', term_resolved='Test3',
|
||||
definition_raw='Test1', definition_resolved='Test2')
|
||||
alias='X3',
|
||||
schema=self.rsform_owned.item,
|
||||
order=2,
|
||||
term_raw='Test3',
|
||||
term_resolved='Test3',
|
||||
definition_raw='Test1',
|
||||
definition_resolved='Test2'
|
||||
)
|
||||
|
||||
def test_retrieve(self):
|
||||
response = self.client.get(f'/api/constituents/{self.cst1.id}')
|
||||
|
@ -421,8 +435,18 @@ class TestRSFormViewset(APITestCase):
|
|||
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)
|
||||
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, format='json'
|
||||
|
@ -452,21 +476,29 @@ class TestRSFormViewset(APITestCase):
|
|||
|
||||
def test_rename_constituenta(self):
|
||||
cst1 = Constituenta.objects.create(
|
||||
alias='X1', schema=self.owned.item, order=1, convention='Test',
|
||||
term_raw='Test1', term_resolved='Test1',
|
||||
alias='X1',
|
||||
schema=self.owned.item,
|
||||
order=1,
|
||||
convention='Test',
|
||||
term_raw='Test1',
|
||||
term_resolved='Test1',
|
||||
term_forms=[{'text':'form1', 'tags':'sing,datv'}]
|
||||
)
|
||||
cst2 = Constituenta.objects.create(
|
||||
alias='X2', schema=self.unowned.item, order=1, convention='Test1',
|
||||
term_raw='Test2', term_resolved='Test2'
|
||||
alias='X2',
|
||||
schema=self.unowned.item,
|
||||
order=1
|
||||
)
|
||||
cst3 = Constituenta.objects.create(
|
||||
alias='X3', schema=self.owned.item, order=2,
|
||||
term_raw='Test3', term_resolved='Test3',
|
||||
definition_raw='Test1', definition_resolved='Test2'
|
||||
alias='X3',
|
||||
schema=self.owned.item, order=2,
|
||||
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(
|
||||
f'/api/rsforms/{self.unowned.item.id}/cst-rename',
|
||||
data=data, format='json'
|
||||
|
@ -479,14 +511,14 @@ class TestRSFormViewset(APITestCase):
|
|||
)
|
||||
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(
|
||||
f'/api/rsforms/{self.owned.item.id}/cst-rename',
|
||||
data=data, format='json'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
data = {'alias': cst3.alias, 'id': cst1.pk}
|
||||
data = {'id': cst1.pk, 'alias': cst3.alias}
|
||||
response = self.client.patch(
|
||||
f'/api/rsforms/{self.owned.item.id}/cst-rename',
|
||||
data=data, format='json'
|
||||
|
@ -520,6 +552,74 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(cst1.alias, 'D2')
|
||||
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):
|
||||
data = {
|
||||
'alias': 'X3',
|
||||
|
|
|
@ -195,7 +195,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
def get_permissions(self):
|
||||
''' Determine permission class. '''
|
||||
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]
|
||||
else:
|
||||
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(
|
||||
summary='delete constituents',
|
||||
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 . import models
|
||||
from . import messages as msg
|
||||
|
||||
|
||||
class NonFieldErrorSerializer(serializers.Serializer):
|
||||
|
@ -36,17 +37,13 @@ class LoginSerializer(serializers.Serializer):
|
|||
password=password
|
||||
)
|
||||
if not user:
|
||||
msg = 'Неправильное сочетание имени пользователя и пароля.'
|
||||
raise serializers.ValidationError(msg, code='authorization')
|
||||
raise serializers.ValidationError(
|
||||
msg.passwordAuthFailed(),
|
||||
code='authorization'
|
||||
)
|
||||
attrs['user'] = user
|
||||
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):
|
||||
''' Serializer: Authorization data. '''
|
||||
|
@ -108,12 +105,6 @@ class ChangePasswordSerializer(serializers.Serializer):
|
|||
old_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):
|
||||
''' Serializer: Create user profile. '''
|
||||
|
@ -136,7 +127,9 @@ class SignupSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate(self, attrs):
|
||||
if attrs['password'] != attrs['password2']:
|
||||
raise serializers.ValidationError({"password": "Введенные пароли не совпадают"})
|
||||
raise serializers.ValidationError({
|
||||
'password': msg.passwordsNotMatch()
|
||||
})
|
||||
return attrs
|
||||
|
||||
def create(self, validated_data):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'''
|
||||
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 .syntax import RuSyntax
|
||||
|
@ -56,9 +56,9 @@ def inflect(text: str, target_grams: str) -> str:
|
|||
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. '''
|
||||
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:
|
||||
|
|
|
@ -25,11 +25,11 @@ class EntityReference:
|
|||
|
||||
|
||||
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.offset = referal_offset
|
||||
self.offset = referral_offset
|
||||
|
||||
def get_type(self) -> ReferenceType:
|
||||
return ReferenceType.syntactic
|
||||
|
|
|
@ -34,31 +34,31 @@ def resolve_entity(ref: EntityReference, context: TermContext) -> str:
|
|||
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. '''
|
||||
offset = ref.offset
|
||||
mainref: Optional['ResolvedReference'] = None
|
||||
master: Optional['ResolvedReference'] = None
|
||||
if offset > 0:
|
||||
index += 1
|
||||
while index < len(allrefs):
|
||||
if isinstance(allrefs[index].ref, EntityReference):
|
||||
while index < len(references):
|
||||
if isinstance(references[index].ref, EntityReference):
|
||||
if offset == 1:
|
||||
mainref = allrefs[index]
|
||||
master = references[index]
|
||||
else:
|
||||
offset -= 1
|
||||
index += 1
|
||||
else:
|
||||
index -= 1
|
||||
while index >= 0:
|
||||
if isinstance(allrefs[index].ref, EntityReference):
|
||||
if isinstance(references[index].ref, EntityReference):
|
||||
if offset == -1:
|
||||
mainref = allrefs[index]
|
||||
master = references[index]
|
||||
else:
|
||||
offset += 1
|
||||
index -= 1
|
||||
if mainref is None:
|
||||
if master is None:
|
||||
return f'!Некорректное смещение: {ref.offset}!'
|
||||
return inflect_dependant(ref.nominal, mainref.resolved)
|
||||
return inflect_dependant(ref.nominal, master.resolved)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
@ -149,7 +149,7 @@ class PhraseParser:
|
|||
_SINGLE_SCORE_SEARCH = 0.2
|
||||
_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
|
||||
|
||||
def parse(self, text: str,
|
||||
|
@ -194,7 +194,7 @@ class PhraseParser:
|
|||
return (start, token.stop)
|
||||
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. '''
|
||||
target = self.parse(text)
|
||||
if not target:
|
||||
|
@ -203,8 +203,8 @@ class PhraseParser:
|
|||
if not target_morpho or not target_morpho.can_coordinate:
|
||||
return text
|
||||
|
||||
model_after = self.parse(cntxt_after)
|
||||
model_before = self.parse(cntxt_before)
|
||||
model_after = self.parse(after)
|
||||
model_before = self.parse(before)
|
||||
etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after)
|
||||
if not etalon:
|
||||
return text
|
||||
|
|
|
@ -40,14 +40,14 @@ class TestResolver(unittest.TestCase):
|
|||
def test_resolve_syntactic(self):
|
||||
ref = ResolvedReference(ref=EntityReference('X1', 'sing,datv'), resolved='человеку')
|
||||
allrefs = [ref, ref, ref, ref]
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 0, allrefs), '!Некорректное смещение: -1!')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 3, allrefs), '!Некорректное смещение: 1!')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=1), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=2), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=3), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-1), 3, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-2), 3, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referal_offset=-3), 3, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 0, allrefs), '!Некорректное смещение: -1!')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 3, allrefs), '!Некорректное смещение: 1!')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=1), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=2), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=3), 0, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-1), 3, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-2), 3, allrefs), 'умному')
|
||||
self.assertEqual(resolve_syntactic(SyntacticReference(text='умный', referral_offset=-3), 3, allrefs), 'умному')
|
||||
|
||||
def test_resolve_invalid(self):
|
||||
self.assertEqual(self.resolver.resolve(''), '')
|
||||
|
|
Loading…
Reference in New Issue
Block a user