Refactor insert functions for RSForm

Add option to insert copy constituents
This commit is contained in:
IRBorisov 2024-03-22 17:01:14 +03:00
parent a6822e2f2b
commit 68b6891ae4
5 changed files with 135 additions and 67 deletions

View File

@ -10,7 +10,7 @@ def renameTrivial(name: str):
def substituteTrivial(name: str): def substituteTrivial(name: str):
return f'Отождествление конституенты с собой не корректно: {name}' return f'Отождествление конституенты с собой не корректно: {name}'
def renameTaken(name: str): def aliasTaken(name: str):
return f'Имя уже используется: {name}' return f'Имя уже используется: {name}'
def pyconceptFailure(): def pyconceptFailure():
@ -25,7 +25,7 @@ def libraryTypeUnexpected():
def exteorFileVersionNotSupported(): def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
def positionNegative(): def invalidPosition():
return 'Invalid position: should be positive integer' return 'Invalid position: should be positive integer'
def constituentaNoStructure(): def constituentaNoStructure():

View File

@ -1,5 +1,5 @@
''' Models: RSForm API. ''' ''' Models: RSForm API. '''
from typing import Iterable, Optional, cast from typing import Dict, Iterable, Optional, cast
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@ -15,6 +15,9 @@ from ..graph import Graph
from .. import messages as msg from .. import messages as msg
_INSERT_LAST: int = -1
class RSForm: class RSForm:
''' RSForm is math form of conceptual schema. ''' ''' RSForm is math form of conceptual schema. '''
def __init__(self, item: LibraryItem): def __init__(self, item: LibraryItem):
@ -87,17 +90,13 @@ class RSForm:
return result return result
@transaction.atomic @transaction.atomic
def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta': def insert_new(self, alias: str, insert_type: CstType, position: int = _INSERT_LAST) -> Constituenta:
''' Insert new constituenta at given position. ''' Insert new constituenta at given position.
All following constituents order is shifted by 1 position. ''' All following constituents order is shifted by 1 position. '''
if position <= 0:
raise ValidationError(msg.positionNegative())
if self.constituents().filter(alias=alias).exists(): if self.constituents().filter(alias=alias).exists():
raise ValidationError(msg.renameTaken(alias)) raise ValidationError(msg.aliasTaken(alias))
currentSize = self.constituents().count() position = self._get_insert_position(position)
position = max(1, min(position, currentSize + 1))
self._shift_positions(position, 1) self._shift_positions(position, 1)
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self.item, schema=self.item,
order=position, order=position,
@ -109,25 +108,49 @@ class RSForm:
return result return result
@transaction.atomic @transaction.atomic
def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': def insert_copy(self, items: list[Constituenta], position: int = _INSERT_LAST) -> list[Constituenta]:
''' Insert new constituenta at last position. ''' ''' Insert copy of target constituents updating references. '''
if self.constituents().filter(alias=alias).exists(): count = len(items)
raise ValidationError(msg.renameTaken(alias)) if count == 0:
position = 1 return []
if self.constituents().exists():
position += self.constituents().count() position = self._get_insert_position(position)
result = Constituenta.objects.create( self._shift_positions(position, count)
schema=self.item,
order=position, indices: Dict[str, int] = {}
alias=alias, for (value, _) in CstType.choices:
cst_type=insert_type indices[value] = self.get_max_index(cast(CstType, value))
)
mapping: Dict[str, str] = {}
for cst in items:
indices[cst.cst_type] = indices[cst.cst_type] + 1
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
mapping[cst.alias] = newAlias
result: list[Constituenta] = []
for cst in items:
newCst = Constituenta.objects.create(
schema=self.item,
order=position,
alias=mapping[cst.alias],
cst_type=cst.cst_type,
convention=cst.convention,
term_raw=cst.term_raw,
term_resolved=cst.term_resolved,
term_forms=cst.term_forms,
definition_raw=cst.definition_raw,
definition_formal=cst.definition_formal,
definition_resolved=cst.definition_resolved
)
newCst.apply_mapping(mapping)
newCst.save()
position = position + 1
result.append(newCst)
self.item.save() self.item.save()
result.refresh_from_db()
return result return result
@transaction.atomic @transaction.atomic
def move_cst(self, listCst: list['Constituenta'], target: int): def move_cst(self, listCst: list[Constituenta], target: int):
''' Move list of constituents to specific position ''' ''' Move list of constituents to specific position '''
count_moved = 0 count_moved = 0
count_top = 0 count_top = 0
@ -159,7 +182,7 @@ class RSForm:
self.item.save() self.item.save()
@transaction.atomic @transaction.atomic
def create_cst(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta': def create_cst(self, data: dict, insert_after: Optional[str]=None) -> Constituenta:
''' Create new cst from data. ''' ''' Create new cst from data. '''
resolver = self.resolver() resolver = self.resolver()
cst = self._insert_new(data, insert_after) cst = self._insert_new(data, insert_after)
@ -182,8 +205,8 @@ class RSForm:
@transaction.atomic @transaction.atomic
def substitute( def substitute(
self, self,
original: 'Constituenta', original: Constituenta,
substitution: 'Constituenta', substitution: Constituenta,
transfer_term: bool transfer_term: bool
): ):
''' Execute constituenta substitution. ''' ''' Execute constituenta substitution. '''
@ -296,6 +319,22 @@ class RSForm:
cst.order += shift cst.order += shift
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])
def _get_last_position(self):
if self.constituents().exists():
return self.constituents().count()
else:
return 0
def _get_insert_position(self, position: int) -> int:
if position <= 0 and position != _INSERT_LAST:
raise ValidationError(msg.invalidPosition())
lastPosition = self._get_last_position()
if position == _INSERT_LAST:
position = lastPosition + 1
else:
position = max(1, min(position, lastPosition + 1))
return position
@transaction.atomic @transaction.atomic
def _reset_order(self): def _reset_order(self):
order = 1 order = 1
@ -305,12 +344,12 @@ class RSForm:
cst.save() cst.save()
order += 1 order += 1
def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> 'Constituenta': def _insert_new(self, data: dict, insert_after: Optional[str]=None) -> Constituenta:
if insert_after is not None: if insert_after is not None:
cst_after = Constituenta.objects.get(pk=insert_after) cst_after = Constituenta.objects.get(pk=insert_after)
return self.insert_at(cst_after.order + 1, data['alias'], data['cst_type']) return self.insert_new(data['alias'], data['cst_type'], cst_after.order + 1)
else: else:
return self.insert_last(data['alias'], data['cst_type']) return self.insert_new(data['alias'], data['cst_type'])
def _term_graph(self) -> Graph: def _term_graph(self) -> Graph:
result = Graph() result = Graph()

View File

@ -260,7 +260,7 @@ class CstRenameSerializer(serializers.Serializer):
}) })
if RSForm(schema).constituents().filter(alias=new_alias).exists(): if RSForm(schema).constituents().filter(alias=new_alias).exists():
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.renameTaken(new_alias) 'alias': msg.aliasTaken(new_alias)
}) })
return attrs return attrs

View File

@ -47,14 +47,14 @@ class TestRSFormViewset(APITestCase):
def test_contents(self): def test_contents(self):
schema = RSForm.create(title='Title1') schema = RSForm.create(title='Title1')
schema.insert_last(alias='X1', insert_type=CstType.BASE) schema.insert_new(alias='X1', insert_type=CstType.BASE)
response = self.client.get(f'/api/rsforms/{schema.item.id}/contents') response = self.client.get(f'/api/rsforms/{schema.item.id}/contents')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_details(self): def test_details(self):
schema = RSForm.create(title='Test', owner=self.user) schema = RSForm.create(title='Test', owner=self.user)
x1 = schema.insert_at(1, 'X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE, 1)
x2 = schema.insert_at(2, 'X2', CstType.BASE) x2 = schema.insert_new('X2', CstType.BASE, 2)
x1.term_raw = 'человек' x1.term_raw = 'человек'
x1.term_resolved = 'человек' x1.term_resolved = 'человек'
x2.term_raw = '@{X1|plur}' x2.term_raw = '@{X1|plur}'
@ -78,7 +78,7 @@ class TestRSFormViewset(APITestCase):
def test_check(self): def test_check(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_new('X1', CstType.BASE, 1)
data = {'expression': 'X1=X1'} data = {'expression': 'X1=X1'}
response = self.client.post( response = self.client.post(
f'/api/rsforms/{schema.item.id}/check', f'/api/rsforms/{schema.item.id}/check',
@ -99,7 +99,7 @@ class TestRSFormViewset(APITestCase):
def test_resolve(self): def test_resolve(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_at(1, 'X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE, 1)
x1.term_resolved = 'синий слон' x1.term_resolved = 'синий слон'
x1.save() x1.save()
data = {'text': '@{1|редкий} @{X1|plur,datv}'} data = {'text': '@{1|редкий} @{X1|plur,datv}'}
@ -139,7 +139,7 @@ class TestRSFormViewset(APITestCase):
def test_export_trs(self): def test_export_trs(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_new('X1', CstType.BASE, 1)
response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs') response = self.client.get(f'/api/rsforms/{schema.item.id}/export-trs')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')

View File

@ -170,17 +170,17 @@ class TestRSForm(TestCase):
def test_insert_at(self): def test_insert_at(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_at(1, 'X1', CstType.BASE) cst1 = schema.insert_new('X1', CstType.BASE, 1)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(cst1.schema, schema.item) self.assertEqual(cst1.schema, schema.item)
cst2 = schema.insert_at(1, 'X2', CstType.BASE) cst2 = schema.insert_new('X2', CstType.BASE, 1)
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst2.schema, schema.item) self.assertEqual(cst2.schema, schema.item)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
cst3 = schema.insert_at(4, 'X3', CstType.BASE) cst3 = schema.insert_new('X3', CstType.BASE, 4)
cst2.refresh_from_db() cst2.refresh_from_db()
cst1.refresh_from_db() cst1.refresh_from_db()
self.assertEqual(cst3.order, 3) self.assertEqual(cst3.order, 3)
@ -188,7 +188,7 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.order, 1) self.assertEqual(cst2.order, 1)
self.assertEqual(cst1.order, 2) self.assertEqual(cst1.order, 2)
cst4 = schema.insert_at(3, 'X4', CstType.BASE) cst4 = schema.insert_new('X4', CstType.BASE, 3)
cst3.refresh_from_db() cst3.refresh_from_db()
cst2.refresh_from_db() cst2.refresh_from_db()
cst1.refresh_from_db() cst1.refresh_from_db()
@ -201,40 +201,40 @@ class TestRSForm(TestCase):
def test_insert_at_invalid_position(self): def test_insert_at_invalid_position(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
schema.insert_at(0, 'X5', CstType.BASE) schema.insert_new('X5', CstType.BASE, 0)
def test_insert_at_invalid_alias(self): def test_insert_at_invalid_alias(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_new('X1', CstType.BASE, 1)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
schema.insert_at(2, 'X1', CstType.BASE) schema.insert_new('X1', CstType.BASE, 2)
def test_insert_at_reorder(self): def test_insert_at_reorder(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_at(1, 'X1', CstType.BASE) schema.insert_new('X1', CstType.BASE, 1)
d1 = schema.insert_at(2, 'D1', CstType.TERM) d1 = schema.insert_new('D1', CstType.TERM, 2)
d2 = schema.insert_at(1, 'D2', CstType.TERM) d2 = schema.insert_new('D2', CstType.TERM, 1)
d1.refresh_from_db() d1.refresh_from_db()
self.assertEqual(d1.order, 3) self.assertEqual(d1.order, 3)
self.assertEqual(d2.order, 1) self.assertEqual(d2.order, 1)
x2 = schema.insert_at(4, 'X2', CstType.BASE) x2 = schema.insert_new('X2', CstType.BASE, 4)
self.assertEqual(x2.order, 4) self.assertEqual(x2.order, 4)
def test_insert_last(self): def test_insert_last(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_last('X1', CstType.BASE) cst1 = schema.insert_new('X1', CstType.BASE)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
self.assertEqual(cst1.schema, schema.item) self.assertEqual(cst1.schema, schema.item)
cst2 = schema.insert_last('X2', CstType.BASE) cst2 = schema.insert_new('X2', CstType.BASE)
self.assertEqual(cst2.order, 2) self.assertEqual(cst2.order, 2)
self.assertEqual(cst2.schema, schema.item) self.assertEqual(cst2.schema, schema.item)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
def test_create_cst_resolve(self): def test_create_cst_resolve(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
cst1 = schema.insert_last('X1', CstType.BASE) cst1 = schema.insert_new('X1', CstType.BASE)
cst1.term_raw = '@{X2|datv}' cst1.term_raw = '@{X2|datv}'
cst1.definition_raw = '@{X1|datv} @{X2|datv}' cst1.definition_raw = '@{X1|datv} @{X2|datv}'
cst1.save() cst1.save()
@ -250,11 +250,40 @@ 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_insert_copy(self):
schema = RSForm.create(title='Test')
x1 = schema.insert_new('X10', CstType.BASE)
s1 = schema.insert_new('S11', CstType.STRUCTURED)
x1.convention = 'Test'
s1.definition_formal = x1.alias
s1.definition_raw = '@{X10|plur}'
x1.save()
s1.save()
result = schema.insert_copy([s1, x1], 2)
self.assertEqual(len(result), 2)
s1.refresh_from_db()
self.assertEqual(s1.order, 4)
x2 = result[1]
self.assertEqual(x2.order, 3)
self.assertEqual(x2.alias, 'X11')
self.assertEqual(x2.cst_type, CstType.BASE)
self.assertEqual(x2.convention, x1.convention)
s2 = result[0]
self.assertEqual(s2.order, 2)
self.assertEqual(s2.alias, 'S12')
self.assertEqual(s2.cst_type, CstType.STRUCTURED)
self.assertEqual(s2.definition_formal, x2.alias)
self.assertEqual(s2.definition_raw, '@{X11|plur}')
def test_apply_mapping(self): def test_apply_mapping(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE)
x2 = schema.insert_last('X11', CstType.BASE) x2 = schema.insert_new('X11', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_new('D1', CstType.TERM)
d1.definition_formal = 'X1 = X11 = X2' d1.definition_formal = 'X1 = X11 = X2'
d1.definition_raw = '@{X11|sing}' d1.definition_raw = '@{X11|sing}'
d1.convention = 'X1' d1.convention = 'X1'
@ -272,9 +301,9 @@ class TestRSForm(TestCase):
def test_substitute(self): def test_substitute(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_new('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_new('D1', CstType.TERM)
d1.definition_formal = x1.alias d1.definition_formal = x1.alias
d1.save() d1.save()
x1.term_raw = 'Test' x1.term_raw = 'Test'
@ -291,10 +320,10 @@ class TestRSForm(TestCase):
def test_move_cst(self): def test_move_cst(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_new('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM) d1 = schema.insert_new('D1', CstType.TERM)
d2 = schema.insert_last('D2', CstType.TERM) d2 = schema.insert_new('D2', CstType.TERM)
schema.move_cst([x2, d2], 1) schema.move_cst([x2, d2], 1)
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
@ -307,8 +336,8 @@ class TestRSForm(TestCase):
def test_move_cst_down(self): def test_move_cst_down(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE) x1 = schema.insert_new('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE) x2 = schema.insert_new('X2', CstType.BASE)
schema.move_cst([x1], 2) schema.move_cst([x1], 2)
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
@ -317,9 +346,9 @@ class TestRSForm(TestCase):
def test_reset_aliases(self): def test_reset_aliases(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
x1 = schema.insert_last('X11', CstType.BASE) x1 = schema.insert_new('X11', CstType.BASE)
x2 = schema.insert_last('X21', CstType.BASE) x2 = schema.insert_new('X21', CstType.BASE)
d1 = schema.insert_last('D11', CstType.TERM) d1 = schema.insert_new('D11', CstType.TERM)
x1.term_raw = 'человек' x1.term_raw = 'человек'
x1.term_resolved = 'человек' x1.term_resolved = 'человек'
d1.convention = 'D11 - cool' d1.convention = 'D11 - cool'