From 68b6891ae43227fcc734ff7cdb7ec10681e53718 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:01:14 +0300 Subject: [PATCH] Refactor insert functions for RSForm Add option to insert copy constituents --- rsconcept/backend/apps/rsform/messages.py | 4 +- .../backend/apps/rsform/models/api_RSForm.py | 97 +++++++++++++------ .../apps/rsform/serializers/data_access.py | 2 +- .../apps/rsform/tests/s_views/t_rsforms.py | 12 +-- .../backend/apps/rsform/tests/t_models.py | 87 +++++++++++------ 5 files changed, 135 insertions(+), 67 deletions(-) diff --git a/rsconcept/backend/apps/rsform/messages.py b/rsconcept/backend/apps/rsform/messages.py index 5a3e1b1a..7c293f14 100644 --- a/rsconcept/backend/apps/rsform/messages.py +++ b/rsconcept/backend/apps/rsform/messages.py @@ -10,7 +10,7 @@ def renameTrivial(name: str): def substituteTrivial(name: str): return f'Отождествление конституенты с собой не корректно: {name}' -def renameTaken(name: str): +def aliasTaken(name: str): return f'Имя уже используется: {name}' def pyconceptFailure(): @@ -25,7 +25,7 @@ def libraryTypeUnexpected(): def exteorFileVersionNotSupported(): return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' -def positionNegative(): +def invalidPosition(): return 'Invalid position: should be positive integer' def constituentaNoStructure(): diff --git a/rsconcept/backend/apps/rsform/models/api_RSForm.py b/rsconcept/backend/apps/rsform/models/api_RSForm.py index 76dfb4ff..f08d46da 100644 --- a/rsconcept/backend/apps/rsform/models/api_RSForm.py +++ b/rsconcept/backend/apps/rsform/models/api_RSForm.py @@ -1,5 +1,5 @@ ''' Models: RSForm API. ''' -from typing import Iterable, Optional, cast +from typing import Dict, Iterable, Optional, cast from django.db import transaction from django.db.models import QuerySet @@ -15,6 +15,9 @@ from ..graph import Graph from .. import messages as msg +_INSERT_LAST: int = -1 + + class RSForm: ''' RSForm is math form of conceptual schema. ''' def __init__(self, item: LibraryItem): @@ -87,17 +90,13 @@ class RSForm: return result @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. All following constituents order is shifted by 1 position. ''' - if position <= 0: - raise ValidationError(msg.positionNegative()) if self.constituents().filter(alias=alias).exists(): - raise ValidationError(msg.renameTaken(alias)) - currentSize = self.constituents().count() - position = max(1, min(position, currentSize + 1)) + raise ValidationError(msg.aliasTaken(alias)) + position = self._get_insert_position(position) self._shift_positions(position, 1) - result = Constituenta.objects.create( schema=self.item, order=position, @@ -109,25 +108,49 @@ class RSForm: return result @transaction.atomic - def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': - ''' Insert new constituenta at last position. ''' - if self.constituents().filter(alias=alias).exists(): - raise ValidationError(msg.renameTaken(alias)) - position = 1 - if self.constituents().exists(): - position += self.constituents().count() - result = Constituenta.objects.create( - schema=self.item, - order=position, - alias=alias, - cst_type=insert_type - ) + def insert_copy(self, items: list[Constituenta], position: int = _INSERT_LAST) -> list[Constituenta]: + ''' Insert copy of target constituents updating references. ''' + count = len(items) + if count == 0: + return [] + + position = self._get_insert_position(position) + self._shift_positions(position, count) + + indices: Dict[str, int] = {} + for (value, _) in CstType.choices: + 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() - result.refresh_from_db() return result @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 ''' count_moved = 0 count_top = 0 @@ -159,7 +182,7 @@ class RSForm: self.item.save() @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. ''' resolver = self.resolver() cst = self._insert_new(data, insert_after) @@ -182,8 +205,8 @@ class RSForm: @transaction.atomic def substitute( self, - original: 'Constituenta', - substitution: 'Constituenta', + original: Constituenta, + substitution: Constituenta, transfer_term: bool ): ''' Execute constituenta substitution. ''' @@ -296,6 +319,22 @@ class RSForm: cst.order += shift 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 def _reset_order(self): order = 1 @@ -305,12 +344,12 @@ class RSForm: cst.save() 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: 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: - return self.insert_last(data['alias'], data['cst_type']) + return self.insert_new(data['alias'], data['cst_type']) def _term_graph(self) -> Graph: result = Graph() diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 667a4e90..a56acfbc 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -260,7 +260,7 @@ class CstRenameSerializer(serializers.Serializer): }) if RSForm(schema).constituents().filter(alias=new_alias).exists(): raise serializers.ValidationError({ - 'alias': msg.renameTaken(new_alias) + 'alias': msg.aliasTaken(new_alias) }) return attrs diff --git a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py index e844d48f..0c82ea1c 100644 --- a/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py +++ b/rsconcept/backend/apps/rsform/tests/s_views/t_rsforms.py @@ -47,14 +47,14 @@ class TestRSFormViewset(APITestCase): def test_contents(self): 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') self.assertEqual(response.status_code, status.HTTP_200_OK) def test_details(self): schema = RSForm.create(title='Test', owner=self.user) - x1 = schema.insert_at(1, 'X1', CstType.BASE) - x2 = schema.insert_at(2, 'X2', CstType.BASE) + x1 = schema.insert_new('X1', CstType.BASE, 1) + x2 = schema.insert_new('X2', CstType.BASE, 2) x1.term_raw = 'человек' x1.term_resolved = 'человек' x2.term_raw = '@{X1|plur}' @@ -78,7 +78,7 @@ class TestRSFormViewset(APITestCase): def test_check(self): schema = RSForm.create(title='Test') - schema.insert_at(1, 'X1', CstType.BASE) + schema.insert_new('X1', CstType.BASE, 1) data = {'expression': 'X1=X1'} response = self.client.post( f'/api/rsforms/{schema.item.id}/check', @@ -99,7 +99,7 @@ class TestRSFormViewset(APITestCase): def test_resolve(self): 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.save() data = {'text': '@{1|редкий} @{X1|plur,datv}'} @@ -139,7 +139,7 @@ class TestRSFormViewset(APITestCase): def test_export_trs(self): 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') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') diff --git a/rsconcept/backend/apps/rsform/tests/t_models.py b/rsconcept/backend/apps/rsform/tests/t_models.py index 7b852e23..f3727dd9 100644 --- a/rsconcept/backend/apps/rsform/tests/t_models.py +++ b/rsconcept/backend/apps/rsform/tests/t_models.py @@ -170,17 +170,17 @@ class TestRSForm(TestCase): def test_insert_at(self): 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.schema, schema.item) - cst2 = schema.insert_at(1, 'X2', CstType.BASE) + cst2 = schema.insert_new('X2', CstType.BASE, 1) cst1.refresh_from_db() self.assertEqual(cst2.order, 1) self.assertEqual(cst2.schema, schema.item) 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() cst1.refresh_from_db() self.assertEqual(cst3.order, 3) @@ -188,7 +188,7 @@ class TestRSForm(TestCase): self.assertEqual(cst2.order, 1) 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() cst2.refresh_from_db() cst1.refresh_from_db() @@ -201,40 +201,40 @@ class TestRSForm(TestCase): def test_insert_at_invalid_position(self): schema = RSForm.create(title='Test') 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): schema = RSForm.create(title='Test') - schema.insert_at(1, 'X1', CstType.BASE) + schema.insert_new('X1', CstType.BASE, 1) with self.assertRaises(ValidationError): - schema.insert_at(2, 'X1', CstType.BASE) + schema.insert_new('X1', CstType.BASE, 2) def test_insert_at_reorder(self): schema = RSForm.create(title='Test') - schema.insert_at(1, 'X1', CstType.BASE) - d1 = schema.insert_at(2, 'D1', CstType.TERM) - d2 = schema.insert_at(1, 'D2', CstType.TERM) + schema.insert_new('X1', CstType.BASE, 1) + d1 = schema.insert_new('D1', CstType.TERM, 2) + d2 = schema.insert_new('D2', CstType.TERM, 1) d1.refresh_from_db() self.assertEqual(d1.order, 3) 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) def test_insert_last(self): 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.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.schema, schema.item) self.assertEqual(cst1.order, 1) def test_create_cst_resolve(self): 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.definition_raw = '@{X1|datv} @{X2|datv}' cst1.save() @@ -250,11 +250,40 @@ class TestRSForm(TestCase): self.assertEqual(cst2.term_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): 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) + x1 = schema.insert_new('X1', CstType.BASE) + x2 = schema.insert_new('X11', CstType.BASE) + d1 = schema.insert_new('D1', CstType.TERM) d1.definition_formal = 'X1 = X11 = X2' d1.definition_raw = '@{X11|sing}' d1.convention = 'X1' @@ -272,9 +301,9 @@ class TestRSForm(TestCase): 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) + x1 = schema.insert_new('X1', CstType.BASE) + x2 = schema.insert_new('X2', CstType.BASE) + d1 = schema.insert_new('D1', CstType.TERM) d1.definition_formal = x1.alias d1.save() x1.term_raw = 'Test' @@ -291,10 +320,10 @@ class TestRSForm(TestCase): def test_move_cst(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) + x1 = schema.insert_new('X1', CstType.BASE) + x2 = schema.insert_new('X2', CstType.BASE) + d1 = schema.insert_new('D1', CstType.TERM) + d2 = schema.insert_new('D2', CstType.TERM) schema.move_cst([x2, d2], 1) x1.refresh_from_db() x2.refresh_from_db() @@ -307,8 +336,8 @@ class TestRSForm(TestCase): def test_move_cst_down(self): schema = RSForm.create(title='Test') - x1 = schema.insert_last('X1', CstType.BASE) - x2 = schema.insert_last('X2', CstType.BASE) + x1 = schema.insert_new('X1', CstType.BASE) + x2 = schema.insert_new('X2', CstType.BASE) schema.move_cst([x1], 2) x1.refresh_from_db() x2.refresh_from_db() @@ -317,9 +346,9 @@ class TestRSForm(TestCase): def test_reset_aliases(self): schema = RSForm.create(title='Test') - x1 = schema.insert_last('X11', CstType.BASE) - x2 = schema.insert_last('X21', CstType.BASE) - d1 = schema.insert_last('D11', CstType.TERM) + x1 = schema.insert_new('X11', CstType.BASE) + x2 = schema.insert_new('X21', CstType.BASE) + d1 = schema.insert_new('D11', CstType.TERM) x1.term_raw = 'человек' x1.term_resolved = 'человек' d1.convention = 'D11 - cool'