M: Change propogation
This commit is contained in:
parent
5c4c0b38d5
commit
1600c8abd2
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-08-09 13:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('library', '0003_alter_librarytemplate_lib_source'),
|
||||||
|
('oss', '0005_inheritance_operation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operation',
|
||||||
|
name='oss',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='operations', to='library.libraryitem', verbose_name='Схема синтеза'),
|
||||||
|
),
|
||||||
|
]
|
194
rsconcept/backend/apps/oss/models/ChangeManager.py
Normal file
194
rsconcept/backend/apps/oss/models/ChangeManager.py
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
''' Models: Change propagation manager. '''
|
||||||
|
from typing import Optional, cast
|
||||||
|
|
||||||
|
from apps.library.models import LibraryItem
|
||||||
|
from apps.rsform.graph import Graph
|
||||||
|
from apps.rsform.models import INSERT_LAST, Constituenta, CstType, RSForm
|
||||||
|
|
||||||
|
from .Inheritance import Inheritance
|
||||||
|
from .Operation import Operation
|
||||||
|
from .OperationSchema import OperationSchema
|
||||||
|
from .Substitution import Substitution
|
||||||
|
|
||||||
|
AliasMapping = dict[str, Constituenta]
|
||||||
|
|
||||||
|
# TODO: add more variety tests for cascade resolutions model
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeManager:
|
||||||
|
''' Change propagation API. '''
|
||||||
|
class Cache:
|
||||||
|
''' Cache for RSForm constituents. '''
|
||||||
|
|
||||||
|
def __init__(self, oss: OperationSchema):
|
||||||
|
self._oss = oss
|
||||||
|
self._schemas: list[RSForm] = []
|
||||||
|
self._schema_by_id: dict[int, RSForm] = {}
|
||||||
|
|
||||||
|
self.operations = list(oss.operations().only('result_id'))
|
||||||
|
self.operation_by_id = {operation.pk: operation for operation in self.operations}
|
||||||
|
self.graph = Graph[int]()
|
||||||
|
for operation in self.operations:
|
||||||
|
self.graph.add_node(operation.pk)
|
||||||
|
for argument in self._oss.arguments().only('operation_id', 'argument_id'):
|
||||||
|
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||||
|
|
||||||
|
self.is_loaded = False
|
||||||
|
self.substitutions: list[Substitution] = []
|
||||||
|
self.inheritance: dict[int, list[tuple[int, int]]] = {}
|
||||||
|
|
||||||
|
def insert(self, schema: RSForm) -> None:
|
||||||
|
''' Insert new schema. '''
|
||||||
|
if not self._schema_by_id.get(schema.model.pk):
|
||||||
|
self._insert_new(schema)
|
||||||
|
|
||||||
|
def get_schema(self, operation: Operation) -> Optional[RSForm]:
|
||||||
|
''' Get schema by Operation. '''
|
||||||
|
if operation.result_id is None:
|
||||||
|
return None
|
||||||
|
if operation.result_id in self._schema_by_id:
|
||||||
|
return self._schema_by_id[operation.result_id]
|
||||||
|
else:
|
||||||
|
schema = RSForm.from_id(operation.result_id)
|
||||||
|
schema.cache.ensure_loaded()
|
||||||
|
self._insert_new(schema)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def get_operation(self, schema: RSForm) -> Optional[Operation]:
|
||||||
|
''' Get operation by schema. '''
|
||||||
|
for operation in self.operations:
|
||||||
|
if operation.result_id == schema.model.pk:
|
||||||
|
return operation
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ensure_loaded(self) -> None:
|
||||||
|
''' Ensure propagation of changes. '''
|
||||||
|
if self.is_loaded:
|
||||||
|
return
|
||||||
|
self.is_loaded = True
|
||||||
|
self.substitutions = list(self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'))
|
||||||
|
for operation in self.operations:
|
||||||
|
self.inheritance[operation.pk] = []
|
||||||
|
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
|
||||||
|
self.inheritance[item.operation_id].append((item.parent_id, item.child_id))
|
||||||
|
|
||||||
|
def get_successor_for(self, parent_cst: int, operation: int,
|
||||||
|
ignore_substitution: bool = False) -> Optional[int]:
|
||||||
|
''' Get child for parent inside target RSFrom. '''
|
||||||
|
if not ignore_substitution:
|
||||||
|
for sub in self.substitutions:
|
||||||
|
if sub.operation_id == operation and sub.original_id == parent_cst:
|
||||||
|
return sub.substitution_id
|
||||||
|
for item in self.inheritance[operation]:
|
||||||
|
if item[0] == parent_cst:
|
||||||
|
return item[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def insert_inheritance(self, inheritance: Inheritance) -> None:
|
||||||
|
''' Insert new inheritance. '''
|
||||||
|
self.inheritance[inheritance.operation_id].append((inheritance.parent_id, inheritance.child_id))
|
||||||
|
|
||||||
|
def _insert_new(self, schema: RSForm) -> None:
|
||||||
|
self._schemas.append(schema)
|
||||||
|
self._schema_by_id[schema.model.pk] = schema
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, model: LibraryItem):
|
||||||
|
self.oss = OperationSchema(model)
|
||||||
|
self.cache = ChangeManager.Cache(self.oss)
|
||||||
|
|
||||||
|
|
||||||
|
def on_create_cst(self, new_cst: Constituenta, source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions when new constituent is created. '''
|
||||||
|
self.cache.insert(source)
|
||||||
|
depend_aliases = new_cst.extract_references()
|
||||||
|
alias_mapping: AliasMapping = {}
|
||||||
|
for alias in depend_aliases:
|
||||||
|
cst = source.cache.by_alias.get(alias)
|
||||||
|
if cst is not None:
|
||||||
|
alias_mapping[alias] = cst
|
||||||
|
operation = self.cache.get_operation(source)
|
||||||
|
if operation is None:
|
||||||
|
return
|
||||||
|
self._create_cst_cascade(new_cst, operation, alias_mapping)
|
||||||
|
|
||||||
|
def on_change_cst_type(self, target: Constituenta, source: RSForm) -> None:
|
||||||
|
''' Trigger cascade resolutions when new constituent type is changed. '''
|
||||||
|
self.cache.insert(source)
|
||||||
|
operation = self.cache.get_operation(source)
|
||||||
|
if operation is None:
|
||||||
|
return
|
||||||
|
self._change_cst_type_cascade(target.pk, target.cst_type, operation)
|
||||||
|
|
||||||
|
def _change_cst_type_cascade(self, cst_id: int, ctype: CstType, operation: Operation) -> None:
|
||||||
|
children = self.cache.graph.outputs[operation.pk]
|
||||||
|
if len(children) == 0:
|
||||||
|
return
|
||||||
|
self.cache.ensure_loaded()
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
successor_id = self.cache.get_successor_for(cst_id, child_id, ignore_substitution=True)
|
||||||
|
if successor_id is None:
|
||||||
|
continue
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is not None and child_schema.change_cst_type(successor_id, ctype):
|
||||||
|
self._change_cst_type_cascade(successor_id, ctype, child_operation)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_cst_cascade(self, prototype: Constituenta, source: Operation, mapping: AliasMapping) -> None:
|
||||||
|
children = self.cache.graph.outputs[source.pk]
|
||||||
|
if len(children) == 0:
|
||||||
|
return
|
||||||
|
source_schema = self.cache.get_schema(source)
|
||||||
|
assert source_schema is not None
|
||||||
|
for child_id in children:
|
||||||
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
|
if child_schema is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.cache.ensure_loaded()
|
||||||
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
|
alias_mapping = {alias: cst.alias for alias, cst in new_mapping.items()}
|
||||||
|
insert_where = self._determine_insert_position(prototype, child_operation, source_schema, child_schema)
|
||||||
|
new_cst = child_schema.insert_copy([prototype], insert_where, alias_mapping)[0]
|
||||||
|
new_inheritance = Inheritance.objects.create(
|
||||||
|
operation=child_operation,
|
||||||
|
child=new_cst,
|
||||||
|
parent=prototype
|
||||||
|
)
|
||||||
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
|
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||||
|
self._create_cst_cascade(new_cst, child_operation, new_mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def _transform_mapping(self, mapping: AliasMapping, operation: Operation, schema: RSForm) -> AliasMapping:
|
||||||
|
if len(mapping) == 0:
|
||||||
|
return mapping
|
||||||
|
result: AliasMapping = {}
|
||||||
|
for alias, cst in mapping.items():
|
||||||
|
successor_id = self.cache.get_successor_for(cst.pk, operation.pk)
|
||||||
|
if successor_id is None:
|
||||||
|
continue
|
||||||
|
successor = schema.cache.by_id.get(successor_id)
|
||||||
|
if successor is None:
|
||||||
|
continue
|
||||||
|
result[alias] = successor
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _determine_insert_position(
|
||||||
|
self, prototype: Constituenta,
|
||||||
|
operation: Operation,
|
||||||
|
source: RSForm,
|
||||||
|
destination: RSForm
|
||||||
|
) -> int:
|
||||||
|
''' Determine insert_after for new constituenta. '''
|
||||||
|
if prototype.order == 1:
|
||||||
|
return 1
|
||||||
|
prev_cst = source.cache.constituents[prototype.order - 2]
|
||||||
|
inherited_prev_id = self.cache.get_successor_for(
|
||||||
|
source.cache.constituents[prototype.order - 2].pk, operation.pk)
|
||||||
|
if inherited_prev_id is None:
|
||||||
|
return INSERT_LAST
|
||||||
|
prev_cst = destination.cache.by_id[inherited_prev_id]
|
||||||
|
return cast(int, prev_cst.order) + 1
|
|
@ -27,7 +27,7 @@ class Operation(Model):
|
||||||
verbose_name='Схема синтеза',
|
verbose_name='Схема синтеза',
|
||||||
to='library.LibraryItem',
|
to='library.LibraryItem',
|
||||||
on_delete=CASCADE,
|
on_delete=CASCADE,
|
||||||
related_name='items'
|
related_name='operations'
|
||||||
)
|
)
|
||||||
operation_type: CharField = CharField(
|
operation_type: CharField = CharField(
|
||||||
verbose_name='Тип',
|
verbose_name='Тип',
|
||||||
|
|
|
@ -50,6 +50,10 @@ class OperationSchema:
|
||||||
''' Operation substitutions. '''
|
''' Operation substitutions. '''
|
||||||
return Substitution.objects.filter(operation__oss=self.model)
|
return Substitution.objects.filter(operation__oss=self.model)
|
||||||
|
|
||||||
|
def inheritance(self) -> QuerySet[Inheritance]:
|
||||||
|
''' Operation inheritances. '''
|
||||||
|
return Inheritance.objects.filter(operation__oss=self.model)
|
||||||
|
|
||||||
def owned_schemas(self) -> QuerySet[LibraryItem]:
|
def owned_schemas(self) -> QuerySet[LibraryItem]:
|
||||||
''' Get QuerySet containing all result schemas owned by current OSS. '''
|
''' Get QuerySet containing all result schemas owned by current OSS. '''
|
||||||
return LibraryItem.objects.filter(
|
return LibraryItem.objects.filter(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
''' Django: Models. '''
|
''' Django: Models. '''
|
||||||
|
|
||||||
from .Argument import Argument
|
from .Argument import Argument
|
||||||
|
from .ChangeManager import ChangeManager
|
||||||
from .Inheritance import Inheritance
|
from .Inheritance import Inheritance
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation, OperationType
|
||||||
from .OperationSchema import OperationSchema
|
from .OperationSchema import OperationSchema
|
||||||
|
|
|
@ -14,9 +14,9 @@ class TestSynthesisSubstitution(TestCase):
|
||||||
self.oss = OperationSchema.create(alias='T1')
|
self.oss = OperationSchema.create(alias='T1')
|
||||||
|
|
||||||
self.ks1 = RSForm.create(alias='KS1', title='Test1')
|
self.ks1 = RSForm.create(alias='KS1', title='Test1')
|
||||||
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
self.ks1X1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
||||||
self.ks2 = RSForm.create(alias='KS2', title='Test2')
|
self.ks2 = RSForm.create(alias='KS2', title='Test2')
|
||||||
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
self.ks2X1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||||
|
|
||||||
self.operation1 = Operation.objects.create(
|
self.operation1 = Operation.objects.create(
|
||||||
oss=self.oss.model,
|
oss=self.oss.model,
|
||||||
|
@ -46,13 +46,13 @@ class TestSynthesisSubstitution(TestCase):
|
||||||
|
|
||||||
self.substitution = Substitution.objects.create(
|
self.substitution = Substitution.objects.create(
|
||||||
operation=self.operation3,
|
operation=self.operation3,
|
||||||
original=self.ks1x1,
|
original=self.ks1X1,
|
||||||
substitution=self.ks2x1
|
substitution=self.ks2X1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_str(self):
|
def test_str(self):
|
||||||
testStr = f'{self.ks1x1} -> {self.ks2x1}'
|
testStr = f'{self.ks1X1} -> {self.ks2X1}'
|
||||||
self.assertEqual(str(self.substitution), testStr)
|
self.assertEqual(str(self.substitution), testStr)
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ class TestSynthesisSubstitution(TestCase):
|
||||||
|
|
||||||
def test_cascade_delete_original(self):
|
def test_cascade_delete_original(self):
|
||||||
self.assertEqual(Substitution.objects.count(), 1)
|
self.assertEqual(Substitution.objects.count(), 1)
|
||||||
self.ks1x1.delete()
|
self.ks1X1.delete()
|
||||||
self.assertEqual(Substitution.objects.count(), 0)
|
self.assertEqual(Substitution.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
def test_cascade_delete_substitution(self):
|
def test_cascade_delete_substitution(self):
|
||||||
self.assertEqual(Substitution.objects.count(), 1)
|
self.assertEqual(Substitution.objects.count(), 1)
|
||||||
self.ks2x1.delete()
|
self.ks2X1.delete()
|
||||||
self.assertEqual(Substitution.objects.count(), 0)
|
self.assertEqual(Substitution.objects.count(), 0)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
''' Tests for REST API. '''
|
''' Tests for REST API. '''
|
||||||
from .t_change_attributes import *
|
from .t_change_attributes import *
|
||||||
|
from .t_change_constituents import *
|
||||||
from .t_oss import *
|
from .t_oss import *
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
''' Testing API: Change attributes of OSS and RSForms. '''
|
''' Testing API: Change attributes of OSS and RSForms. '''
|
||||||
|
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from apps.library.models import AccessPolicy, Editor, LocationHead
|
from apps.library.models import AccessPolicy, Editor, LocationHead
|
||||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import RSForm
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
''' Testing API: Change constituents in OSS. '''
|
||||||
|
|
||||||
|
from apps.oss.models import OperationSchema, OperationType
|
||||||
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
class TestChangeConstituents(EndpointTester):
|
||||||
|
''' Testing Constituents change propagation in OSS. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.owned = OperationSchema.create(
|
||||||
|
title='Test',
|
||||||
|
alias='T1',
|
||||||
|
owner=self.user
|
||||||
|
)
|
||||||
|
self.owned_id = self.owned.model.pk
|
||||||
|
|
||||||
|
self.ks1 = RSForm.create(
|
||||||
|
alias='KS1',
|
||||||
|
title='Test1',
|
||||||
|
owner=self.user
|
||||||
|
)
|
||||||
|
self.ks1X1 = self.ks1.insert_new('X4')
|
||||||
|
self.ks1X2 = self.ks1.insert_new('X5')
|
||||||
|
|
||||||
|
self.ks2 = RSForm.create(
|
||||||
|
alias='KS2',
|
||||||
|
title='Test2',
|
||||||
|
owner=self.user
|
||||||
|
)
|
||||||
|
self.ks2X1 = self.ks2.insert_new('X1')
|
||||||
|
self.ks2D1 = self.ks2.insert_new(
|
||||||
|
alias='D1',
|
||||||
|
definition_formal=r'X1\X1'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.operation1 = self.owned.create_operation(
|
||||||
|
alias='1',
|
||||||
|
operation_type=OperationType.INPUT,
|
||||||
|
result=self.ks1.model
|
||||||
|
)
|
||||||
|
self.operation2 = self.owned.create_operation(
|
||||||
|
alias='2',
|
||||||
|
operation_type=OperationType.INPUT,
|
||||||
|
result=self.ks2.model
|
||||||
|
)
|
||||||
|
|
||||||
|
self.operation3 = self.owned.create_operation(
|
||||||
|
alias='3',
|
||||||
|
operation_type=OperationType.SYNTHESIS
|
||||||
|
)
|
||||||
|
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2])
|
||||||
|
self.owned.execute_operation(self.operation3)
|
||||||
|
self.operation3.refresh_from_db()
|
||||||
|
self.ks3 = RSForm(self.operation3.result)
|
||||||
|
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||||
|
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
||||||
|
def test_create_constituenta(self):
|
||||||
|
data = {
|
||||||
|
'alias': 'X3',
|
||||||
|
'cst_type': CstType.BASE,
|
||||||
|
'definition_formal': 'X4 = X5'
|
||||||
|
}
|
||||||
|
response = self.executeCreated(data=data, item=self.ks1.model.pk)
|
||||||
|
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
||||||
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
||||||
|
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||||
|
self.assertEqual(self.ks3.constituents().count(), 5)
|
||||||
|
self.assertEqual(inherited_cst.alias, 'X4')
|
||||||
|
self.assertEqual(inherited_cst.order, 3)
|
||||||
|
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
|
||||||
|
|
||||||
|
@decl_endpoint('/api/rsforms/{item}/rename-cst', method='patch')
|
||||||
|
def test_rename_constituenta(self):
|
||||||
|
data = {'target': self.ks1X1.pk, 'alias': 'D21', 'cst_type': CstType.TERM}
|
||||||
|
response = self.executeOK(data=data, item=self.ks1.model.pk)
|
||||||
|
self.ks1X1.refresh_from_db()
|
||||||
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
||||||
|
self.assertEqual(self.ks1X1.alias, data['alias'])
|
||||||
|
self.assertEqual(self.ks1X1.cst_type, data['cst_type'])
|
||||||
|
self.assertEqual(inherited_cst.alias, 'D2')
|
||||||
|
self.assertEqual(inherited_cst.cst_type, data['cst_type'])
|
|
@ -1,8 +1,5 @@
|
||||||
''' Testing API: Operation Schema. '''
|
''' Testing API: Operation Schema. '''
|
||||||
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType, LocationHead
|
|
||||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
@ -28,7 +25,7 @@ class TestOssViewset(EndpointTester):
|
||||||
title='Test1',
|
title='Test1',
|
||||||
owner=self.user
|
owner=self.user
|
||||||
)
|
)
|
||||||
self.ks1x1 = self.ks1.insert_new(
|
self.ks1X1 = self.ks1.insert_new(
|
||||||
'X1',
|
'X1',
|
||||||
term_raw='X1_1',
|
term_raw='X1_1',
|
||||||
term_resolved='X1_1'
|
term_resolved='X1_1'
|
||||||
|
@ -38,7 +35,7 @@ class TestOssViewset(EndpointTester):
|
||||||
title='Test2',
|
title='Test2',
|
||||||
owner=self.user
|
owner=self.user
|
||||||
)
|
)
|
||||||
self.ks2x1 = self.ks2.insert_new(
|
self.ks2X1 = self.ks2.insert_new(
|
||||||
'X2',
|
'X2',
|
||||||
term_raw='X1_2',
|
term_raw='X1_2',
|
||||||
term_resolved='X1_2'
|
term_resolved='X1_2'
|
||||||
|
@ -60,8 +57,8 @@ class TestOssViewset(EndpointTester):
|
||||||
)
|
)
|
||||||
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2])
|
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2])
|
||||||
self.owned.set_substitutions(self.operation3, [{
|
self.owned.set_substitutions(self.operation3, [{
|
||||||
'original': self.ks1x1,
|
'original': self.ks1X1,
|
||||||
'substitution': self.ks2x1
|
'substitution': self.ks2X1
|
||||||
}])
|
}])
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/details', method='get')
|
@decl_endpoint('/api/oss/{item}/details', method='get')
|
||||||
|
@ -85,12 +82,12 @@ class TestOssViewset(EndpointTester):
|
||||||
self.assertEqual(len(response.data['substitutions']), 1)
|
self.assertEqual(len(response.data['substitutions']), 1)
|
||||||
sub = response.data['substitutions'][0]
|
sub = response.data['substitutions'][0]
|
||||||
self.assertEqual(sub['operation'], self.operation3.pk)
|
self.assertEqual(sub['operation'], self.operation3.pk)
|
||||||
self.assertEqual(sub['original'], self.ks1x1.pk)
|
self.assertEqual(sub['original'], self.ks1X1.pk)
|
||||||
self.assertEqual(sub['substitution'], self.ks2x1.pk)
|
self.assertEqual(sub['substitution'], self.ks2X1.pk)
|
||||||
self.assertEqual(sub['original_alias'], self.ks1x1.alias)
|
self.assertEqual(sub['original_alias'], self.ks1X1.alias)
|
||||||
self.assertEqual(sub['original_term'], self.ks1x1.term_resolved)
|
self.assertEqual(sub['original_term'], self.ks1X1.term_resolved)
|
||||||
self.assertEqual(sub['substitution_alias'], self.ks2x1.alias)
|
self.assertEqual(sub['substitution_alias'], self.ks2X1.alias)
|
||||||
self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved)
|
self.assertEqual(sub['substitution_term'], self.ks2X1.term_resolved)
|
||||||
|
|
||||||
arguments = response.data['arguments']
|
arguments = response.data['arguments']
|
||||||
self.assertEqual(len(arguments), 2)
|
self.assertEqual(len(arguments), 2)
|
||||||
|
@ -369,14 +366,14 @@ class TestOssViewset(EndpointTester):
|
||||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||||
'substitutions': [
|
'substitutions': [
|
||||||
{
|
{
|
||||||
'original': self.ks1x1.pk,
|
'original': self.ks1X1.pk,
|
||||||
'substitution': ks3x1.pk
|
'substitution': ks3x1.pk
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
data['substitutions'][0]['substitution'] = self.ks2x1.pk
|
data['substitutions'][0]['substitution'] = self.ks2X1.pk
|
||||||
self.toggle_admin(True)
|
self.toggle_admin(True)
|
||||||
self.executeBadData(data=data, item=self.unowned_id)
|
self.executeBadData(data=data, item=self.unowned_id)
|
||||||
self.logout()
|
self.logout()
|
||||||
|
@ -421,7 +418,7 @@ class TestOssViewset(EndpointTester):
|
||||||
def test_update_operation_invalid_substitution(self):
|
def test_update_operation_invalid_substitution(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
|
|
||||||
self.ks1x2 = self.ks1.insert_new('X2')
|
self.ks1X2 = self.ks1.insert_new('X2')
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'target': self.operation3.pk,
|
'target': self.operation3.pk,
|
||||||
|
@ -434,12 +431,12 @@ class TestOssViewset(EndpointTester):
|
||||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
'arguments': [self.operation1.pk, self.operation2.pk],
|
||||||
'substitutions': [
|
'substitutions': [
|
||||||
{
|
{
|
||||||
'original': self.ks1x1.pk,
|
'original': self.ks1X1.pk,
|
||||||
'substitution': self.ks2x1.pk
|
'substitution': self.ks2X1.pk
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'original': self.ks2x1.pk,
|
'original': self.ks2X1.pk,
|
||||||
'substitution': self.ks1x2.pk
|
'substitution': self.ks1X2.pk
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -473,4 +470,4 @@ class TestOssViewset(EndpointTester):
|
||||||
items = list(RSForm(schema).constituents())
|
items = list(RSForm(schema).constituents())
|
||||||
self.assertEqual(len(items), 1)
|
self.assertEqual(len(items), 1)
|
||||||
self.assertEqual(items[0].alias, 'X1')
|
self.assertEqual(items[0].alias, 'X1')
|
||||||
self.assertEqual(items[0].term_resolved, self.ks2x1.term_resolved)
|
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
''' Models: Constituenta. '''
|
''' Models: Constituenta. '''
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from cctext import extract_entities
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
CASCADE,
|
CASCADE,
|
||||||
|
@ -15,10 +16,16 @@ from django.db.models import (
|
||||||
|
|
||||||
from ..utils import apply_pattern
|
from ..utils import apply_pattern
|
||||||
|
|
||||||
|
_RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line
|
||||||
_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]+)') # cspell:disable-line
|
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
|
||||||
|
|
||||||
|
|
||||||
|
def extract_globals(expression: str) -> set[str]:
|
||||||
|
''' Extract all global aliases from expression. '''
|
||||||
|
return set(re.findall(_RE_GLOBALS, expression))
|
||||||
|
|
||||||
|
|
||||||
class CstType(TextChoices):
|
class CstType(TextChoices):
|
||||||
''' Type of constituenta. '''
|
''' Type of constituenta. '''
|
||||||
BASE = 'basic'
|
BASE = 'basic'
|
||||||
|
@ -120,3 +127,10 @@ class Constituenta(Model):
|
||||||
modified = True
|
modified = True
|
||||||
self.definition_raw = definition
|
self.definition_raw = definition
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
|
def extract_references(self) -> set[str]:
|
||||||
|
''' Extract all references from term and definition. '''
|
||||||
|
result: set[str] = extract_globals(self.definition_formal)
|
||||||
|
result.update(extract_entities(self.term_raw))
|
||||||
|
result.update(extract_entities(self.definition_raw))
|
||||||
|
return result
|
||||||
|
|
|
@ -11,7 +11,6 @@ from shared import messages as msg
|
||||||
|
|
||||||
from ..graph import Graph
|
from ..graph import Graph
|
||||||
from .api_RSLanguage import (
|
from .api_RSLanguage import (
|
||||||
extract_globals,
|
|
||||||
generate_structure,
|
generate_structure,
|
||||||
get_type_prefix,
|
get_type_prefix,
|
||||||
guess_type,
|
guess_type,
|
||||||
|
@ -21,9 +20,9 @@ from .api_RSLanguage import (
|
||||||
is_simple_expression,
|
is_simple_expression,
|
||||||
split_template
|
split_template
|
||||||
)
|
)
|
||||||
from .Constituenta import Constituenta, CstType
|
from .Constituenta import Constituenta, CstType, extract_globals
|
||||||
|
|
||||||
_INSERT_LAST: int = -1
|
INSERT_LAST: int = -1
|
||||||
|
|
||||||
|
|
||||||
class RSForm:
|
class RSForm:
|
||||||
|
@ -54,7 +53,7 @@ class RSForm:
|
||||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||||
self.is_loaded = True
|
self.is_loaded = True
|
||||||
|
|
||||||
def ensure(self) -> None:
|
def ensure_loaded(self) -> None:
|
||||||
if not self.is_loaded:
|
if not self.is_loaded:
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
|
@ -140,7 +139,7 @@ class RSForm:
|
||||||
|
|
||||||
def on_term_change(self, changed: list[int]) -> None:
|
def on_term_change(self, changed: list[int]) -> None:
|
||||||
''' Trigger cascade resolutions when term changes. '''
|
''' Trigger cascade resolutions when term changes. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
graph_terms = self._graph_term()
|
graph_terms = self._graph_term()
|
||||||
expansion = graph_terms.expand_outputs(changed)
|
expansion = graph_terms.expand_outputs(changed)
|
||||||
expanded_change = changed + expansion
|
expanded_change = changed + expansion
|
||||||
|
@ -188,7 +187,7 @@ class RSForm:
|
||||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||||
''' Create new cst from data. '''
|
''' Create new cst from data. '''
|
||||||
if insert_after is None:
|
if insert_after is None:
|
||||||
position = _INSERT_LAST
|
position = INSERT_LAST
|
||||||
else:
|
else:
|
||||||
position = insert_after.order + 1
|
position = insert_after.order + 1
|
||||||
result = self.insert_new(data['alias'], data['cst_type'], position)
|
result = self.insert_new(data['alias'], data['cst_type'], position)
|
||||||
|
@ -217,7 +216,7 @@ class RSForm:
|
||||||
self,
|
self,
|
||||||
alias: str,
|
alias: str,
|
||||||
cst_type: Optional[CstType] = None,
|
cst_type: Optional[CstType] = None,
|
||||||
position: int = _INSERT_LAST,
|
position: int = INSERT_LAST,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> Constituenta:
|
) -> Constituenta:
|
||||||
''' Insert new constituenta at given position.
|
''' Insert new constituenta at given position.
|
||||||
|
@ -239,13 +238,14 @@ class RSForm:
|
||||||
self.save()
|
self.save()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def insert_copy(self, items: list[Constituenta], position: int = _INSERT_LAST) -> list[Constituenta]:
|
def insert_copy(self, items: list[Constituenta], position: int = INSERT_LAST,
|
||||||
|
initial_mapping: Optional[dict[str, str]] = None) -> list[Constituenta]:
|
||||||
''' Insert copy of target constituents updating references. '''
|
''' Insert copy of target constituents updating references. '''
|
||||||
count = len(items)
|
count = len(items)
|
||||||
if count == 0:
|
if count == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
position = self._get_insert_position(position)
|
position = self._get_insert_position(position)
|
||||||
self._shift_positions(position, count)
|
self._shift_positions(position, count)
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ class RSForm:
|
||||||
for (value, _) in CstType.choices:
|
for (value, _) in CstType.choices:
|
||||||
indices[value] = self.get_max_index(cast(CstType, value))
|
indices[value] = self.get_max_index(cast(CstType, value))
|
||||||
|
|
||||||
mapping: dict[str, str] = {}
|
mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
|
||||||
for cst in items:
|
for cst in items:
|
||||||
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
||||||
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
||||||
|
@ -344,9 +344,23 @@ class RSForm:
|
||||||
mapping[cst.alias] = alias
|
mapping[cst.alias] = alias
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
def change_cst_type(self, target: int, new_type: CstType) -> bool:
|
||||||
|
''' Change type of constituenta generating alias automatically. '''
|
||||||
|
self.cache.ensure_loaded()
|
||||||
|
cst = self.cache.by_id.get(target)
|
||||||
|
if cst is None:
|
||||||
|
return False
|
||||||
|
newAlias = f'{get_type_prefix(new_type)}{self.get_max_index(new_type) + 1}'
|
||||||
|
mapping = {cst.alias: newAlias}
|
||||||
|
cst.cst_type = new_type
|
||||||
|
cst.alias = newAlias
|
||||||
|
cst.save(update_fields=['cst_type', 'alias'])
|
||||||
|
self.apply_mapping(mapping)
|
||||||
|
return True
|
||||||
|
|
||||||
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False) -> None:
|
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False) -> None:
|
||||||
''' Apply rename mapping. '''
|
''' Apply rename mapping. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
update_list: list[Constituenta] = []
|
update_list: list[Constituenta] = []
|
||||||
for cst in self.cache.constituents:
|
for cst in self.cache.constituents:
|
||||||
if cst.apply_mapping(mapping, change_aliases):
|
if cst.apply_mapping(mapping, change_aliases):
|
||||||
|
@ -356,7 +370,7 @@ class RSForm:
|
||||||
|
|
||||||
def resolve_all_text(self) -> None:
|
def resolve_all_text(self) -> None:
|
||||||
''' Trigger reference resolution for all texts. '''
|
''' Trigger reference resolution for all texts. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
graph_terms = self._graph_term()
|
graph_terms = self._graph_term()
|
||||||
resolver = Resolver({})
|
resolver = Resolver({})
|
||||||
update_list: list[Constituenta] = []
|
update_list: list[Constituenta] = []
|
||||||
|
@ -395,7 +409,7 @@ class RSForm:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
position = target.order + 1
|
position = target.order + 1
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
self._shift_positions(position, count_new)
|
self._shift_positions(position, count_new)
|
||||||
result = []
|
result = []
|
||||||
cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION
|
cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION
|
||||||
|
@ -432,10 +446,10 @@ class RSForm:
|
||||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||||
|
|
||||||
def _get_insert_position(self, position: int) -> int:
|
def _get_insert_position(self, position: int) -> int:
|
||||||
if position <= 0 and position != _INSERT_LAST:
|
if position <= 0 and position != INSERT_LAST:
|
||||||
raise ValidationError(msg.invalidPosition())
|
raise ValidationError(msg.invalidPosition())
|
||||||
lastPosition = self.constituents().count()
|
lastPosition = self.constituents().count()
|
||||||
if position == _INSERT_LAST:
|
if position == INSERT_LAST:
|
||||||
position = lastPosition + 1
|
position = lastPosition + 1
|
||||||
else:
|
else:
|
||||||
position = max(1, min(position, lastPosition + 1))
|
position = max(1, min(position, lastPosition + 1))
|
||||||
|
@ -458,7 +472,7 @@ class RSForm:
|
||||||
|
|
||||||
def _graph_formal(self) -> Graph[int]:
|
def _graph_formal(self) -> Graph[int]:
|
||||||
''' Graph based on formal definitions. '''
|
''' Graph based on formal definitions. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
result: Graph[int] = Graph()
|
result: Graph[int] = Graph()
|
||||||
for cst in self.cache.constituents:
|
for cst in self.cache.constituents:
|
||||||
result.add_node(cst.pk)
|
result.add_node(cst.pk)
|
||||||
|
@ -471,7 +485,7 @@ class RSForm:
|
||||||
|
|
||||||
def _graph_term(self) -> Graph[int]:
|
def _graph_term(self) -> Graph[int]:
|
||||||
''' Graph based on term texts. '''
|
''' Graph based on term texts. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
result: Graph[int] = Graph()
|
result: Graph[int] = Graph()
|
||||||
for cst in self.cache.constituents:
|
for cst in self.cache.constituents:
|
||||||
result.add_node(cst.pk)
|
result.add_node(cst.pk)
|
||||||
|
@ -484,7 +498,7 @@ class RSForm:
|
||||||
|
|
||||||
def _graph_text(self) -> Graph[int]:
|
def _graph_text(self) -> Graph[int]:
|
||||||
''' Graph based on definition texts. '''
|
''' Graph based on definition texts. '''
|
||||||
self.cache.ensure()
|
self.cache.ensure_loaded()
|
||||||
result: Graph[int] = Graph()
|
result: Graph[int] = Graph()
|
||||||
for cst in self.cache.constituents:
|
for cst in self.cache.constituents:
|
||||||
result.add_node(cst.pk)
|
result.add_node(cst.pk)
|
||||||
|
@ -500,7 +514,7 @@ class SemanticInfo:
|
||||||
''' Semantic information derived from constituents. '''
|
''' Semantic information derived from constituents. '''
|
||||||
|
|
||||||
def __init__(self, schema: RSForm):
|
def __init__(self, schema: RSForm):
|
||||||
schema.cache.ensure()
|
schema.cache.ensure_loaded()
|
||||||
self._graph = schema._graph_formal()
|
self._graph = schema._graph_formal()
|
||||||
self._items = schema.cache.constituents
|
self._items = schema.cache.constituents
|
||||||
self._cst_by_ID = schema.cache.by_id
|
self._cst_by_ID = schema.cache.by_id
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
''' Django: Models. '''
|
''' Django: Models. '''
|
||||||
|
|
||||||
from .Constituenta import Constituenta, CstType
|
from .Constituenta import Constituenta, CstType, extract_globals
|
||||||
from .RSForm import RSForm
|
from .RSForm import INSERT_LAST, RSForm
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from enum import IntEnum, unique
|
from enum import IntEnum, unique
|
||||||
from typing import Set, Tuple, cast
|
from typing import Tuple, cast
|
||||||
|
|
||||||
import pyconcept
|
import pyconcept
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ from shared import messages as msg
|
||||||
|
|
||||||
from .Constituenta import CstType
|
from .Constituenta import CstType
|
||||||
|
|
||||||
_RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line
|
|
||||||
_RE_TEMPLATE = r'R\d+'
|
_RE_TEMPLATE = r'R\d+'
|
||||||
_RE_COMPLEX_SYMBOLS = r'[∀∃×ℬ;|:]'
|
_RE_COMPLEX_SYMBOLS = r'[∀∃×ℬ;|:]'
|
||||||
|
|
||||||
|
@ -67,11 +66,6 @@ def is_functional(cst_type: CstType) -> bool:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def extract_globals(expression: str) -> Set[str]:
|
|
||||||
''' Extract all global aliases from expression. '''
|
|
||||||
return set(re.findall(_RE_GLOBALS, expression))
|
|
||||||
|
|
||||||
|
|
||||||
def guess_type(alias: str) -> CstType:
|
def guess_type(alias: str) -> CstType:
|
||||||
''' Get CstType for alias. '''
|
''' Get CstType for alias. '''
|
||||||
prefix = alias[0]
|
prefix = alias[0]
|
||||||
|
|
|
@ -121,7 +121,7 @@ class RSFormSerializer(serializers.ModelSerializer):
|
||||||
for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)):
|
for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)):
|
||||||
result['inheritance'].append([link.child.pk, link.parent.pk])
|
result['inheritance'].append([link.child.pk, link.parent.pk])
|
||||||
result['oss'] = []
|
result['oss'] = []
|
||||||
for oss in LibraryItem.objects.filter(items__result=instance).only('alias'):
|
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
||||||
result['oss'].append({
|
result['oss'].append({
|
||||||
'id': oss.pk,
|
'id': oss.pk,
|
||||||
'alias': oss.alias
|
'alias': oss.alias
|
||||||
|
@ -246,7 +246,7 @@ class CstTargetSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class CstRenameSerializer(serializers.Serializer):
|
class CstRenameSerializer(serializers.Serializer):
|
||||||
''' Serializer: Constituenta renaming. '''
|
''' Serializer: Constituenta renaming. '''
|
||||||
target = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
|
target = PKField(many=False, queryset=Constituenta.objects.only('alias', 'cst_type', 'schema'))
|
||||||
alias = serializers.CharField()
|
alias = serializers.CharField()
|
||||||
cst_type = serializers.CharField()
|
cst_type = serializers.CharField()
|
||||||
|
|
||||||
|
|
|
@ -58,3 +58,28 @@ class TestConstituenta(TestCase):
|
||||||
self.assertEqual(cst.term_forms, [])
|
self.assertEqual(cst.term_forms, [])
|
||||||
self.assertEqual(cst.definition_resolved, '')
|
self.assertEqual(cst.definition_resolved, '')
|
||||||
self.assertEqual(cst.definition_raw, '')
|
self.assertEqual(cst.definition_raw, '')
|
||||||
|
|
||||||
|
def test_extract_references(self):
|
||||||
|
cst = Constituenta.objects.create(
|
||||||
|
alias='X1',
|
||||||
|
order=1,
|
||||||
|
schema=self.schema1.model,
|
||||||
|
definition_formal='X1 X2',
|
||||||
|
term_raw='@{X3|sing} is a @{X4|sing}',
|
||||||
|
definition_raw='@{X5|sing}'
|
||||||
|
)
|
||||||
|
self.assertEqual(cst.extract_references(), set(['X1', 'X2', 'X3', 'X4', 'X5']))
|
||||||
|
|
||||||
|
def text_apply_mapping(self):
|
||||||
|
cst = Constituenta.objects.create(
|
||||||
|
alias='X1',
|
||||||
|
order=1,
|
||||||
|
schema=self.schema1.model,
|
||||||
|
definition_formal='X1 = X2',
|
||||||
|
term_raw='@{X1|sing}',
|
||||||
|
definition_raw='@{X2|sing}'
|
||||||
|
)
|
||||||
|
cst.apply_mapping({'X1': 'X3', 'X2': 'X4'})
|
||||||
|
self.assertEqual(cst.definition_formal, 'X3 = X4')
|
||||||
|
self.assertEqual(cst.term_raw, '@{X3|sing}')
|
||||||
|
self.assertEqual(cst.definition_raw, '@{X4|sing}')
|
||||||
|
|
|
@ -183,9 +183,10 @@ class TestRSFormViewset(EndpointTester):
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
||||||
def test_create_constituenta(self):
|
def test_create_constituenta(self):
|
||||||
data = {'alias': 'X3'}
|
data = {'alias': 'X3', 'cst_type': CstType.BASE}
|
||||||
self.executeForbidden(data=data, item=self.unowned_id)
|
self.executeForbidden(data=data, item=self.unowned_id)
|
||||||
|
|
||||||
|
data = {'alias': 'X3'}
|
||||||
self.owned.insert_new('X1')
|
self.owned.insert_new('X1')
|
||||||
x2 = self.owned.insert_new('X2')
|
x2 = self.owned.insert_new('X2')
|
||||||
self.executeBadData(item=self.owned_id)
|
self.executeBadData(item=self.owned_id)
|
||||||
|
|
|
@ -16,6 +16,7 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||||
from apps.library.serializers import LibraryItemSerializer
|
from apps.library.serializers import LibraryItemSerializer
|
||||||
|
from apps.oss.models import ChangeManager
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared import permissions, utility
|
from shared import permissions, utility
|
||||||
|
@ -76,7 +77,6 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['post'], url_path='create-cst')
|
@action(detail=True, methods=['post'], url_path='create-cst')
|
||||||
def create_cst(self, request: Request, pk) -> HttpResponse:
|
def create_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Create new constituenta. '''
|
''' Create new constituenta. '''
|
||||||
schema = self._get_item()
|
|
||||||
serializer = s.CstCreateSerializer(data=request.data)
|
serializer = s.CstCreateSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
|
@ -86,13 +86,18 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
insert_after = data['insert_after']
|
insert_after = data['insert_after']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
new_cst = m.RSForm(schema).create_cst(data, insert_after)
|
schema = m.RSForm(self._get_item())
|
||||||
|
new_cst = schema.create_cst(data, insert_after)
|
||||||
|
hosts = LibraryItem.objects.filter(operations__result=schema.model)
|
||||||
|
for host in hosts:
|
||||||
|
ChangeManager(host).on_create_cst(new_cst, schema)
|
||||||
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data={
|
data={
|
||||||
'new_cst': s.CstSerializer(new_cst).data,
|
'new_cst': s.CstSerializer(new_cst).data,
|
||||||
'schema': s.RSFormParseSerializer(schema).data
|
'schema': s.RSFormParseSerializer(schema.model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -119,7 +124,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'schema': msg.constituentaNotInRSform(schema.title)
|
'schema': msg.constituentaNotInRSform(schema.title)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
serializer.update(instance=cst, validated_data=serializer.validated_data)
|
serializer.update(instance=cst, validated_data=serializer.validated_data)
|
||||||
|
# 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
|
@ -140,9 +148,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='produce-structure')
|
@action(detail=True, methods=['patch'], url_path='produce-structure')
|
||||||
def produce_structure(self, request: Request, pk) -> HttpResponse:
|
def produce_structure(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Produce a term for every element of the target constituenta typification. '''
|
''' Produce a term for every element of the target constituenta typification. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
|
|
||||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema})
|
serializer = s.CstTargetSerializer(data=request.data, context={'schema': model})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||||
if cst.cst_type not in [m.CstType.FUNCTION, m.CstType.STRUCTURED, m.CstType.TERM]:
|
if cst.cst_type not in [m.CstType.FUNCTION, m.CstType.STRUCTURED, m.CstType.TERM]:
|
||||||
|
@ -150,7 +158,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
f'{cst.pk}': msg.constituentaNoStructure()
|
f'{cst.pk}': msg.constituentaNoStructure()
|
||||||
})
|
})
|
||||||
|
|
||||||
schema_details = s.RSFormParseSerializer(schema).data['items']
|
schema_details = s.RSFormParseSerializer(model).data['items']
|
||||||
cst_parse = next(item for item in schema_details if item['id'] == cst.pk)['parse']
|
cst_parse = next(item for item in schema_details if item['id'] == cst.pk)['parse']
|
||||||
if not cst_parse['typification']:
|
if not cst_parse['typification']:
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -158,12 +166,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
||||||
)
|
)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
result = m.RSForm(schema).produce_structure(cst, cst_parse)
|
result = m.RSForm(model).produce_structure(cst, cst_parse)
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'cst_list': result,
|
'cst_list': result,
|
||||||
'schema': s.RSFormParseSerializer(schema).data
|
'schema': s.RSFormParseSerializer(model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -181,26 +189,31 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='rename-cst')
|
@action(detail=True, methods=['patch'], url_path='rename-cst')
|
||||||
def rename_cst(self, request: Request, pk) -> HttpResponse:
|
def rename_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Rename constituenta possibly changing type. '''
|
''' Rename constituenta possibly changing type. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
|
serializer = s.CstRenameSerializer(data=request.data, context={'schema': model})
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||||
|
changed_type = cst.cst_type != serializer.validated_data['cst_type']
|
||||||
mapping = {cst.alias: serializer.validated_data['alias']}
|
mapping = {cst.alias: serializer.validated_data['alias']}
|
||||||
cst.alias = serializer.validated_data['alias']
|
cst.alias = serializer.validated_data['alias']
|
||||||
cst.cst_type = serializer.validated_data['cst_type']
|
cst.cst_type = serializer.validated_data['cst_type']
|
||||||
|
schema = m.RSForm(model)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
cst.save()
|
cst.save()
|
||||||
m.RSForm(schema).apply_mapping(mapping=mapping, change_aliases=False)
|
schema.apply_mapping(mapping=mapping, change_aliases=False)
|
||||||
|
|
||||||
schema.refresh_from_db()
|
|
||||||
cst.refresh_from_db()
|
cst.refresh_from_db()
|
||||||
|
if changed_type:
|
||||||
|
hosts = LibraryItem.objects.filter(operations__result=model)
|
||||||
|
for host in hosts:
|
||||||
|
ChangeManager(host).on_change_cst_type(cst, schema)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'new_cst': s.CstSerializer(cst).data,
|
'new_cst': s.CstSerializer(cst).data,
|
||||||
'schema': s.RSFormParseSerializer(schema).data
|
'schema': s.RSFormParseSerializer(model).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -218,10 +231,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='substitute')
|
@action(detail=True, methods=['patch'], url_path='substitute')
|
||||||
def substitute(self, request: Request, pk) -> HttpResponse:
|
def substitute(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Substitute occurrences of constituenta with another one. '''
|
''' Substitute occurrences of constituenta with another one. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstSubstituteSerializer(
|
serializer = s.CstSubstituteSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'schema': schema}
|
context={'schema': model}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
@ -231,12 +244,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
original = cast(m.Constituenta, substitution['original'])
|
original = cast(m.Constituenta, substitution['original'])
|
||||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||||
substitutions.append((original, replacement))
|
substitutions.append((original, replacement))
|
||||||
m.RSForm(schema).substitute(substitutions)
|
m.RSForm(model).substitute(substitutions)
|
||||||
|
|
||||||
schema.refresh_from_db()
|
model.refresh_from_db()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(schema).data
|
data=s.RSFormParseSerializer(model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -253,17 +266,17 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
||||||
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Delete multiple constituents. '''
|
''' Endpoint: Delete multiple constituents. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstListSerializer(
|
serializer = s.CstListSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'schema': schema}
|
context={'schema': model}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
m.RSForm(schema).delete_cst(serializer.validated_data['items'])
|
m.RSForm(model).delete_cst(serializer.validated_data['items'])
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(schema).data
|
data=s.RSFormParseSerializer(model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -280,20 +293,20 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='move-cst')
|
@action(detail=True, methods=['patch'], url_path='move-cst')
|
||||||
def move_cst(self, request: Request, pk) -> HttpResponse:
|
def move_cst(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Move multiple constituents. '''
|
''' Endpoint: Move multiple constituents. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
serializer = s.CstMoveSerializer(
|
serializer = s.CstMoveSerializer(
|
||||||
data=request.data,
|
data=request.data,
|
||||||
context={'schema': schema}
|
context={'schema': model}
|
||||||
)
|
)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
m.RSForm(schema).move_cst(
|
m.RSForm(model).move_cst(
|
||||||
target=serializer.validated_data['items'],
|
target=serializer.validated_data['items'],
|
||||||
destination=serializer.validated_data['move_to']
|
destination=serializer.validated_data['move_to']
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(schema).data
|
data=s.RSFormParseSerializer(model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -309,11 +322,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||||
def reset_aliases(self, request: Request, pk) -> HttpResponse:
|
def reset_aliases(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Recreate all aliases based on order. '''
|
''' Endpoint: Recreate all aliases based on order. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
m.RSForm(schema).reset_aliases()
|
m.RSForm(model).reset_aliases()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(schema).data
|
data=s.RSFormParseSerializer(model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -329,11 +342,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['patch'], url_path='restore-order')
|
@action(detail=True, methods=['patch'], url_path='restore-order')
|
||||||
def restore_order(self, request: Request, pk) -> HttpResponse:
|
def restore_order(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Restore order based on types and term graph. '''
|
''' Endpoint: Restore order based on types and term graph. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
m.RSForm(schema).restore_order()
|
m.RSForm(model).restore_order()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(schema).data
|
data=s.RSFormParseSerializer(model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
@ -353,10 +366,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
input_serializer = s.RSFormUploadSerializer(data=request.data)
|
input_serializer = s.RSFormUploadSerializer(data=request.data)
|
||||||
input_serializer.is_valid(raise_exception=True)
|
input_serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
load_metadata = input_serializer.validated_data['load_metadata']
|
load_metadata = input_serializer.validated_data['load_metadata']
|
||||||
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||||
data['id'] = schema.pk
|
data['id'] = model.pk
|
||||||
|
|
||||||
serializer = s.RSFormTRSSerializer(
|
serializer = s.RSFormTRSSerializer(
|
||||||
data=data,
|
data=data,
|
||||||
|
@ -461,10 +474,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
@action(detail=True, methods=['get'], url_path='export-trs')
|
@action(detail=True, methods=['get'], url_path='export-trs')
|
||||||
def export_trs(self, request: Request, pk) -> HttpResponse:
|
def export_trs(self, request: Request, pk) -> HttpResponse:
|
||||||
''' Endpoint: Download Exteor compatible file. '''
|
''' Endpoint: Download Exteor compatible file. '''
|
||||||
schema = self._get_item()
|
model = self._get_item()
|
||||||
data = s.RSFormTRSSerializer(m.RSForm(schema)).data
|
data = s.RSFormTRSSerializer(m.RSForm(model)).data
|
||||||
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
||||||
filename = utils.filename_for_schema(schema.alias)
|
filename = utils.filename_for_schema(model.alias)
|
||||||
response = HttpResponse(file, content_type='application/zip')
|
response = HttpResponse(file, content_type='application/zip')
|
||||||
response['Content-Disposition'] = f'attachment; filename={filename}'
|
response['Content-Disposition'] = f'attachment; filename={filename}'
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -25,7 +25,7 @@ function TextArea({
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
{
|
{
|
||||||
'flex flex-col flex-grow gap-2': !dense,
|
'flex flex-col gap-2': !dense,
|
||||||
'flex flex-grow items-center gap-3': dense
|
'flex flex-grow items-center gap-3': dense
|
||||||
},
|
},
|
||||||
dense && className
|
dense && className
|
||||||
|
|
|
@ -155,10 +155,7 @@ export interface ICstMovetoData extends IConstituentaList {
|
||||||
export interface ICstUpdateData
|
export interface ICstUpdateData
|
||||||
extends Pick<IConstituentaMeta, 'id'>,
|
extends Pick<IConstituentaMeta, 'id'>,
|
||||||
Partial<
|
Partial<
|
||||||
Pick<
|
Pick<IConstituentaMeta, 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'>
|
||||||
IConstituentaMeta,
|
|
||||||
'alias' | 'convention' | 'definition_formal' | 'definition_raw' | 'term_raw' | 'term_forms'
|
|
||||||
>
|
|
||||||
> {}
|
> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -55,7 +55,6 @@ function FormConstituenta({
|
||||||
}: FormConstituentaProps) {
|
}: FormConstituentaProps) {
|
||||||
const { schema, cstUpdate, processing } = useRSForm();
|
const { schema, cstUpdate, processing } = useRSForm();
|
||||||
|
|
||||||
const [alias, setAlias] = useState('');
|
|
||||||
const [term, setTerm] = useState('');
|
const [term, setTerm] = useState('');
|
||||||
const [textDefinition, setTextDefinition] = useState('');
|
const [textDefinition, setTextDefinition] = useState('');
|
||||||
const [expression, setExpression] = useState('');
|
const [expression, setExpression] = useState('');
|
||||||
|
@ -98,7 +97,6 @@ function FormConstituenta({
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (state) {
|
if (state) {
|
||||||
setAlias(state.alias);
|
|
||||||
setConvention(state.convention || '');
|
setConvention(state.convention || '');
|
||||||
setTerm(state.term_raw || '');
|
setTerm(state.term_raw || '');
|
||||||
setTextDefinition(state.definition_raw || '');
|
setTextDefinition(state.definition_raw || '');
|
||||||
|
@ -117,7 +115,6 @@ function FormConstituenta({
|
||||||
}
|
}
|
||||||
const data: ICstUpdateData = {
|
const data: ICstUpdateData = {
|
||||||
id: state.id,
|
id: state.id,
|
||||||
alias: alias,
|
|
||||||
term_raw: term,
|
term_raw: term,
|
||||||
definition_formal: expression,
|
definition_formal: expression,
|
||||||
definition_raw: textDefinition,
|
definition_raw: textDefinition,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user