Compare commits

..

No commits in common. "2a306613559e8b07d19aedafeb17ce6a9c0dda76" and "01c0eb201ebc3d9b41e2d8397350345c19612a1c" have entirely different histories.

67 changed files with 792 additions and 2036 deletions

View File

@ -432,8 +432,7 @@ disable=too-many-public-methods,
missing-function-docstring, missing-function-docstring,
attribute-defined-outside-init, attribute-defined-outside-init,
ungrouped-imports, ungrouped-imports,
abstract-method, abstract-method
fixme
# Enable the message, report, category or checker with the given id(s). You can # Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option # either give multiple identifier separated by comma (,) or put this option

View File

@ -140,7 +140,7 @@ class LibraryItem(Model):
def _update_connected_operations(self): def _update_connected_operations(self):
# using method level import to prevent circular dependency # using method level import to prevent circular dependency
from apps.oss.models import Operation # pylint: disable=import-outside-toplevel from apps.oss.models import Operation # pylint: disable=import-outside-toplevel
operations = Operation.objects.filter(result__pk=self.pk) operations = Operation.objects.filter(result__pk=self.pk, sync_text=True)
if not operations.exists(): if not operations.exists():
return return
for operation in operations: for operation in operations:

View File

@ -21,7 +21,7 @@ class ArgumentAdmin(admin.ModelAdmin):
class SynthesisSubstitutionAdmin(admin.ModelAdmin): class SynthesisSubstitutionAdmin(admin.ModelAdmin):
''' Admin model: Substitutions as part of Synthesis operation. ''' ''' Admin model: Substitutions as part of Synthesis operation. '''
ordering = ['operation'] ordering = ['operation']
list_display = ['id', 'operation', 'original', 'substitution'] list_display = ['id', 'operation', 'original', 'substitution', 'transfer_term']
search_fields = ['id', 'operation', 'original', 'substitution'] search_fields = ['id', 'operation', 'original', 'substitution']

View File

@ -1,17 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-30 07:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('oss', '0002_inheritance'),
]
operations = [
migrations.RemoveField(
model_name='operation',
name='sync_text',
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-30 07:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('oss', '0003_remove_operation_sync_text'),
]
operations = [
migrations.RemoveField(
model_name='substitution',
name='transfer_term',
),
]

View File

@ -2,18 +2,15 @@
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
SET_NULL, SET_NULL,
BooleanField,
CharField, CharField,
FloatField, FloatField,
ForeignKey, ForeignKey,
Model, Model,
QuerySet,
TextChoices, TextChoices,
TextField TextField
) )
from .Argument import Argument
from .Substitution import Substitution
class OperationType(TextChoices): class OperationType(TextChoices):
''' Type of operation. ''' ''' Type of operation. '''
@ -42,6 +39,10 @@ class Operation(Model):
on_delete=SET_NULL, on_delete=SET_NULL,
related_name='producer' related_name='producer'
) )
sync_text: BooleanField = BooleanField(
verbose_name='Синхронизация',
default=True
)
alias: CharField = CharField( alias: CharField = CharField(
verbose_name='Шифр', verbose_name='Шифр',
@ -73,11 +74,3 @@ class Operation(Model):
def __str__(self) -> str: def __str__(self) -> str:
return f'Операция {self.alias}' return f'Операция {self.alias}'
def getArguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation=self)
def getSubstitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. '''
return Substitution.objects.filter(operation=self)

View File

@ -5,12 +5,10 @@ from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from apps.library.models import Editor, LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import RSForm
from shared import messages as msg from shared import messages as msg
from .Argument import Argument from .Argument import Argument
from .Inheritance import Inheritance
from .Operation import Operation from .Operation import Operation
from .Substitution import Substitution from .Substitution import Substitution
@ -78,8 +76,8 @@ class OperationSchema:
''' Delete operation. ''' ''' Delete operation. '''
operation.delete() operation.delete()
# TODO: deal with attached schema # deal with attached schema
# TODO: trigger on_change effects # trigger on_change effects
self.save() self.save()
@ -88,124 +86,53 @@ class OperationSchema:
''' Set input schema for operation. ''' ''' Set input schema for operation. '''
if schema == target.result: if schema == target.result:
return return
target.result = schema if schema:
if schema is not None:
target.result = schema target.result = schema
target.alias = schema.alias target.alias = schema.alias
target.title = schema.title target.title = schema.title
target.comment = schema.comment target.comment = schema.comment
else:
target.result = None
target.save() target.save()
# TODO: trigger on_change effects # trigger on_change effects
self.save() self.save()
@transaction.atomic @transaction.atomic
def set_arguments(self, operation: Operation, arguments: list[Operation]): def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]:
''' Set arguments to operation. ''' ''' Add Argument to operation. '''
processed: list[Operation] = [] if Argument.objects.filter(operation=operation, argument=argument).exists():
changed = False return None
for current in operation.getArguments(): result = Argument.objects.create(operation=operation, argument=argument)
if current.argument not in arguments: self.save()
changed = True return result
current.delete()
else: @transaction.atomic
processed.append(current.argument) def clear_arguments(self, target: Operation):
for arg in arguments: ''' Clear all arguments for operation. '''
if arg not in processed: if not Argument.objects.filter(operation=target).exists():
changed = True
processed.append(arg)
Argument.objects.create(operation=operation, argument=arg)
if not changed:
return return
# TODO: trigger on_change effects
Argument.objects.filter(operation=target).delete()
Substitution.objects.filter(operation=target).delete()
# trigger on_change effects
self.save() self.save()
@transaction.atomic @transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]): def set_substitutions(self, target: Operation, substitutes: list[dict]):
''' Clear all arguments for operation. ''' ''' Clear all arguments for operation. '''
processed: list[dict] = [] Substitution.objects.filter(operation=target).delete()
changed = False
for current in target.getSubstitutions():
subs = [
x for x in substitutes
if x['original'] == current.original and x['substitution'] == current.substitution
]
if len(subs) == 0:
changed = True
current.delete()
else:
processed.append(subs[0])
for sub in substitutes: for sub in substitutes:
if sub not in processed: Substitution.objects.create(
changed = True operation=target,
Substitution.objects.create( original=sub['original'],
operation=target, substitution=sub['substitution'],
original=sub['original'], transfer_term=sub['transfer_term']
substitution=sub['substitution']
)
if not changed:
return
# TODO: trigger on_change effects
self.save()
@transaction.atomic
def create_input(self, operation: Operation) -> RSForm:
''' Create input RSForm. '''
schema = RSForm.create(
owner=self.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=self.model.access_policy,
location=self.model.location
)
Editor.set(schema.model, self.model.editors())
operation.result = schema.model
operation.save()
self.save()
return schema
@transaction.atomic
def execute_operation(self, operation: Operation) -> bool:
''' Execute target operation. '''
schemas: list[LibraryItem] = [arg.argument.result for arg in operation.getArguments()]
if None in schemas:
return False
substitutions = operation.getSubstitutions()
receiver = self.create_input(operation)
parents: dict = {}
children: dict = {}
for operand in schemas:
schema = RSForm(operand)
items = list(schema.constituents())
new_items = receiver.insert_copy(items)
for (i, cst) in enumerate(new_items):
parents[cst.pk] = items[i]
children[items[i].pk] = cst
for sub in substitutions:
original = children[sub.original.pk]
replacement = children[sub.substitution.pk]
receiver.substitute(original, replacement)
# TODO: remove duplicates from diamond
for cst in receiver.constituents():
parent = parents.get(cst.id)
assert parent is not None
Inheritance.objects.create(
child=cst,
parent=parent
) )
receiver.restore_order() # trigger on_change effects
receiver.reset_aliases()
self.save() self.save()
return True

View File

@ -1,5 +1,5 @@
''' Models: Synthesis Substitution. ''' ''' Models: Synthesis Substitution. '''
from django.db.models import CASCADE, ForeignKey, Model from django.db.models import CASCADE, BooleanField, ForeignKey, Model
class Substitution(Model): class Substitution(Model):
@ -22,6 +22,10 @@ class Substitution(Model):
on_delete=CASCADE, on_delete=CASCADE,
related_name='as_substitute' related_name='as_substitute'
) )
transfer_term: BooleanField = BooleanField(
verbose_name='Перенос термина',
default=False
)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''

View File

@ -6,8 +6,6 @@ from .data_access import (
OperationCreateSerializer, OperationCreateSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,
OperationTargetSerializer, OperationTargetSerializer
OperationUpdateSerializer,
SetOperationInputSerializer
) )
from .responses import NewOperationResponse, NewSchemaResponse from .responses import NewOperationResponse, NewSchemaResponse

View File

@ -21,6 +21,7 @@ class SubstitutionExSerializer(serializers.Serializer):
operation = serializers.IntegerField() operation = serializers.IntegerField()
original = serializers.IntegerField() original = serializers.IntegerField()
substitution = serializers.IntegerField() substitution = serializers.IntegerField()
transfer_term = serializers.BooleanField()
original_alias = serializers.CharField() original_alias = serializers.CharField()
original_term = serializers.CharField() original_term = serializers.CharField()
substitution_alias = serializers.CharField() substitution_alias = serializers.CharField()

View File

@ -5,10 +5,8 @@ from django.db.models import F
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem
from apps.library.serializers import LibraryItemDetailsSerializer from apps.library.serializers import LibraryItemDetailsSerializer
from apps.rsform.models import Constituenta
from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType from ..models import Argument, Operation, OperationSchema, OperationType
@ -34,7 +32,7 @@ class ArgumentSerializer(serializers.ModelSerializer):
class OperationCreateSerializer(serializers.Serializer): class OperationCreateSerializer(serializers.Serializer):
''' Serializer: Operation creation. ''' ''' Serializer: Operation creation. '''
class OperationCreateData(serializers.ModelSerializer): class OperationData(serializers.ModelSerializer):
''' Serializer: Operation creation data. ''' ''' Serializer: Operation creation data. '''
alias = serializers.CharField() alias = serializers.CharField()
operation_type = serializers.ChoiceField(OperationType.choices) operation_type = serializers.ChoiceField(OperationType.choices)
@ -43,83 +41,18 @@ class OperationCreateSerializer(serializers.Serializer):
''' serializer metadata. ''' ''' serializer metadata. '''
model = Operation model = Operation
fields = \ fields = \
'alias', 'operation_type', 'title', \ 'alias', 'operation_type', 'title', 'sync_text', \
'comment', 'result', 'position_x', 'position_y' 'comment', 'result', 'position_x', 'position_y'
create_schema = serializers.BooleanField(default=False, required=False) create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationCreateData() item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False) arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
positions = serializers.ListField( positions = serializers.ListField(
child=OperationPositionSerializer(), child=OperationPositionSerializer(),
default=[] default=[]
) )
class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
class OperationUpdateData(serializers.ModelSerializer):
''' Serializer: Operation creation data. '''
class Meta:
''' serializer metadata. '''
model = Operation
fields = 'alias', 'title', 'comment'
target = PKField(many=False, queryset=Operation.objects.all())
item_data = OperationUpdateData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
substitutions = serializers.ListField(
child=SubstitutionSerializerBase(),
required=False
)
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
if 'arguments' not in attrs:
return attrs
oss = cast(LibraryItem, self.context['oss'])
for operation in attrs['arguments']:
if operation.oss != oss:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS(oss.title)
})
if 'substitutions' not in attrs:
return attrs
schemas = [arg.result.pk for arg in attrs['arguments'] if arg.result is not None]
substitutions = attrs['substitutions']
to_delete = {x['original'].pk for x in substitutions}
deleted = set()
for item in substitutions:
original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution'])
if original_cst.schema.pk not in schemas:
raise serializers.ValidationError({
f'{original_cst.id}': msg.constituentaNotFromOperation()
})
if substitution_cst.schema.pk not in schemas:
raise serializers.ValidationError({
f'{substitution_cst.id}': msg.constituentaNotFromOperation()
})
if original_cst.pk in deleted or substitution_cst.pk in to_delete:
raise serializers.ValidationError({
f'{original_cst.id}': msg.substituteDouble(original_cst.alias)
})
if original_cst.schema == substitution_cst.schema:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
deleted.add(original_cst.pk)
return attrs
class OperationTargetSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())
@ -133,36 +66,9 @@ class OperationTargetSerializer(serializers.Serializer):
operation = cast(Operation, attrs['target']) operation = cast(Operation, attrs['target'])
if oss and operation.oss != oss: if oss and operation.oss != oss:
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title) f'{operation.id}': msg.operationNotOwned(oss.title)
})
return attrs
class SetOperationInputSerializer(serializers.Serializer):
''' Serializer: Set input schema for operation. '''
target = PKField(many=False, queryset=Operation.objects.all())
input = PKField(
many=False,
queryset=LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM),
allow_null=True,
default=None
)
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target'])
if oss and operation.oss != oss:
raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title)
})
if operation.operation_type != OperationType.INPUT:
raise serializers.ValidationError({
'target': msg.operationNotInput(operation.alias)
}) })
self.instance = operation
return attrs return attrs
@ -197,6 +103,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
'operation', 'operation',
'original', 'original',
'substitution', 'substitution',
'transfer_term',
original_alias=F('original__alias'), original_alias=F('original__alias'),
original_term=F('original__term_resolved'), original_term=F('original__term_resolved'),
substitution_alias=F('substitution__alias'), substitution_alias=F('substitution__alias'),

View File

@ -29,6 +29,7 @@ class TestOperation(TestCase):
self.assertEqual(self.operation.alias, 'KS1') self.assertEqual(self.operation.alias, 'KS1')
self.assertEqual(self.operation.title, '') self.assertEqual(self.operation.title, '')
self.assertEqual(self.operation.comment, '') self.assertEqual(self.operation.comment, '')
self.assertEqual(self.operation.sync_text, True)
self.assertEqual(self.operation.position_x, 0) self.assertEqual(self.operation.position_x, 0)
self.assertEqual(self.operation.position_y, 0) self.assertEqual(self.operation.position_y, 0)
@ -49,6 +50,15 @@ class TestOperation(TestCase):
self.assertEqual(self.operation.title, schema.model.title) self.assertEqual(self.operation.title, schema.model.title)
self.assertEqual(self.operation.comment, schema.model.comment) self.assertEqual(self.operation.comment, schema.model.comment)
self.operation.sync_text = False
self.operation.save()
schema.model.alias = 'KS3'
schema.save()
self.operation.refresh_from_db()
self.assertEqual(self.operation.result, schema.model)
self.assertNotEqual(self.operation.alias, schema.model.alias)
def test_sync_from_library_item(self): def test_sync_from_library_item(self):
schema = LibraryItem.objects.create(alias=self.operation.alias, item_type=LibraryItemType.RSFORM) schema = LibraryItem.objects.create(alias=self.operation.alias, item_type=LibraryItemType.RSFORM)
self.operation.result = schema self.operation.result = schema

View File

@ -47,7 +47,8 @@ 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,
transfer_term=False
) )

View File

@ -2,7 +2,7 @@
from rest_framework import status from rest_framework import status
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType, LocationHead from apps.library.models import AccessPolicy, 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
@ -23,27 +23,10 @@ class TestOssViewset(EndpointTester):
def populateData(self): def populateData(self):
self.ks1 = RSForm.create( self.ks1 = RSForm.create(alias='KS1', title='Test1', owner=self.user)
alias='KS1', self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
title='Test1', self.ks2 = RSForm.create(alias='KS2', title='Test2', owner=self.user)
owner=self.user self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
)
self.ks1x1 = self.ks1.insert_new(
'X1',
term_raw='X1_1',
term_resolved='X1_1'
)
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user
)
self.ks2x1 = self.ks2.insert_new(
'X2',
term_raw='X1_2',
term_resolved='X1_2'
)
self.operation1 = self.owned.create_operation( self.operation1 = self.owned.create_operation(
alias='1', alias='1',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
@ -58,10 +41,12 @@ class TestOssViewset(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) self.owned.add_argument(self.operation3, self.operation1)
self.owned.add_argument(self.operation3, 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,
'transfer_term': False
}]) }])
@decl_endpoint('/api/oss/{item}/details', method='get') @decl_endpoint('/api/oss/{item}/details', method='get')
@ -87,6 +72,7 @@ class TestOssViewset(EndpointTester):
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['transfer_term'], False)
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)
@ -149,6 +135,7 @@ class TestOssViewset(EndpointTester):
'alias': 'Test3', 'alias': 'Test3',
'title': 'Test title', 'title': 'Test title',
'comment': 'Тест кириллицы', 'comment': 'Тест кириллицы',
'sync_text': False,
'position_x': 1, 'position_x': 1,
'position_y': 1, 'position_y': 1,
}, },
@ -173,6 +160,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(new_operation['comment'], data['item_data']['comment']) self.assertEqual(new_operation['comment'], data['item_data']['comment'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x']) self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y']) self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['sync_text'], data['item_data']['sync_text'])
self.assertEqual(new_operation['result'], None) self.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x']) self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
@ -220,7 +208,6 @@ class TestOssViewset(EndpointTester):
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_schema(self): def test_create_operation_schema(self):
self.populateData() self.populateData()
Editor.add(self.owned.model, self.user2)
data = { data = {
'item_data': { 'item_data': {
'alias': 'Test4', 'alias': 'Test4',
@ -241,7 +228,6 @@ class TestOssViewset(EndpointTester):
self.assertEqual(schema.visible, False) self.assertEqual(schema.visible, False)
self.assertEqual(schema.access_policy, self.owned.model.access_policy) self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location) self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.editors())
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self): def test_delete_operation(self):
@ -287,11 +273,13 @@ class TestOssViewset(EndpointTester):
self.operation1.result = None self.operation1.result = None
self.operation1.comment = 'TestComment' self.operation1.comment = 'TestComment'
self.operation1.title = 'TestTitle' self.operation1.title = 'TestTitle'
self.operation1.sync_text = False
self.operation1.save() self.operation1.save()
response = self.executeOK(data=data) response = self.executeOK(data=data)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
new_schema = response.data['new_schema'] new_schema = response.data['new_schema']
self.assertEqual(self.operation1.sync_text, True)
self.assertEqual(new_schema['id'], self.operation1.result.pk) self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias) self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title) self.assertEqual(new_schema['title'], self.operation1.title)
@ -299,178 +287,3 @@ class TestOssViewset(EndpointTester):
data['target'] = self.operation3.pk data['target'] = self.operation3.pk
self.executeBadData(data=data) self.executeBadData(data=data)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
data['input'] = self.ks1.model.pk
self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421'
self.ks1.model.comment = 'TestComment42'
self.ks1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
self.assertEqual(self.operation1.title, self.ks1.model.title)
self.assertEqual(self.operation1.comment, self.ks1.model.comment)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_change_schema(self):
self.populateData()
self.operation2.result = None
data = {
'positions': [],
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
response = self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.assertEqual(self.operation2.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1x1.pk,
'substitution': ks3x1.pk
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2x1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.comment, data['item_data']['comment'])
self.assertEqual(set([argument.pk for argument in self.operation3.getArguments()]), set(data['arguments']))
sub = self.operation3.getSubstitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.comment, data['item_data']['comment'])
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.comment, data['item_data']['comment'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self):
self.populateData()
self.ks1x2 = self.ks1.insert_new('X2')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod'
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1x1.pk,
'substitution': self.ks2x1.pk
},
{
'original': self.ks2x1.pk,
'substitution': self.ks1x2.pk
}
]
}
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
def test_execute_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': [],
'target': self.operation1.pk
}
self.executeBadData(data=data)
data['target'] = self.operation3.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeOK(data=data)
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
self.assertEqual(schema.comment, self.operation3.comment)
self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents())
self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2x1.term_resolved)

View File

@ -35,10 +35,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'create_operation', 'create_operation',
'delete_operation', 'delete_operation',
'update_positions', 'update_positions',
'create_input', 'create_input'
'set_input',
'update_operation',
'execute_operation'
]: ]:
permission_list = [permissions.ItemEditor] permission_list = [permissions.ItemEditor]
elif self.action in ['details']: elif self.action in ['details']:
@ -103,14 +100,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
new_operation = oss.create_operation(**serializer.validated_data['item_data']) data: dict = serializer.validated_data['item_data']
if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']: if data['operation_type'] == m.OperationType.INPUT and serializer.validated_data['create_schema']:
oss.create_input(new_operation) schema = LibraryItem.objects.create(
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: item_type=LibraryItemType.RSFORM,
oss.set_arguments( owner=oss.model.owner,
operation=new_operation, alias=data['alias'],
arguments=serializer.validated_data['arguments'] title=data['title'],
comment=data['comment'],
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
) )
data['result'] = schema
new_operation = oss.create_operation(**data)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']:
oss.add_argument(operation=new_operation, argument=argument)
oss.refresh_from_db()
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
@ -144,6 +152,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
oss.delete_operation(serializer.validated_data['target']) oss.delete_operation(serializer.validated_data['target'])
oss.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data
@ -182,127 +191,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_positions(serializer.validated_data['positions']) oss.update_positions(serializer.validated_data['positions'])
schema = oss.create_input(operation) schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
operation.result = schema
operation.sync_text = True
operation.save()
oss.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'new_schema': LibraryItemSerializer(schema.model).data, 'new_schema': LibraryItemSerializer(schema).data,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )
@extend_schema(
summary='set input schema for target operation',
tags=['OSS'],
request=s.SetOperationInputSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='set-input')
def set_input(self, request: Request, pk):
''' Set input schema for target operation. '''
serializer = s.SetOperationInputSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
oss.set_input(operation, serializer.validated_data['input'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='update operation',
tags=['OSS'],
request=s.OperationUpdateSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-operation')
def update_operation(self, request: Request, pk):
''' Update operation arguments and parameters. '''
serializer = s.OperationUpdateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.comment = serializer.validated_data['item_data']['comment']
operation.save()
if operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)
if can_edit:
operation.result.alias = operation.alias
operation.result.title = operation.title
operation.result.comment = operation.comment
operation.result.save()
if 'arguments' in serializer.validated_data:
oss.set_arguments(operation, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation, serializer.validated_data['substitutions'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='execute operation',
tags=['OSS'],
request=s.OperationTargetSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='execute-operation')
def execute_operation(self, request: Request, pk):
''' Execute operation. '''
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.SYNTHESIS:
raise serializers.ValidationError({
'target': msg.operationNotSynthesis(operation.alias)
})
if operation.result is not None:
raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias)
})
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
oss.execute_operation(operation)
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)

View File

@ -12,6 +12,7 @@ from django.db.models import (
TextChoices, TextChoices,
TextField TextField
) )
from django.urls import reverse
from ..utils import apply_pattern from ..utils import apply_pattern
@ -94,6 +95,10 @@ class Constituenta(Model):
verbose_name = 'Конституента' verbose_name = 'Конституента'
verbose_name_plural = 'Конституенты' verbose_name_plural = 'Конституенты'
def get_absolute_url(self):
''' URL access. '''
return reverse('constituenta-detail', kwargs={'pk': self.pk})
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.alias}' return f'{self.alias}'

View File

@ -241,12 +241,18 @@ class RSForm:
def substitute( def substitute(
self, self,
original: Constituenta, original: Constituenta,
substitution: Constituenta substitution: Constituenta,
transfer_term: bool
): ):
''' Execute constituenta substitution. ''' ''' Execute constituenta substitution. '''
assert original.pk != substitution.pk assert original.pk != substitution.pk
mapping = {original.alias: substitution.alias} mapping = {original.alias: substitution.alias}
self.apply_mapping(mapping) self.apply_mapping(mapping)
if transfer_term:
substitution.term_raw = original.term_raw
substitution.term_forms = original.term_forms
substitution.term_resolved = original.term_resolved
substitution.save()
original.delete() original.delete()
self.on_term_change([substitution.id]) self.on_term_change([substitution.id])

View File

@ -19,8 +19,7 @@ from .data_access import (
CstTargetSerializer, CstTargetSerializer,
InlineSynthesisSerializer, InlineSynthesisSerializer,
RSFormParseSerializer, RSFormParseSerializer,
RSFormSerializer, RSFormSerializer
SubstitutionSerializerBase
) )
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter

View File

@ -31,7 +31,7 @@ class CstSerializer(serializers.ModelSerializer):
''' serializer metadata. ''' ''' serializer metadata. '''
model = Constituenta model = Constituenta
fields = '__all__' fields = '__all__'
read_only_fields = ('id', 'schema', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved') read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
def update(self, instance: Constituenta, validated_data) -> Constituenta: def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: use alias for better code readability data = validated_data # Note: use alias for better code readability
@ -212,7 +212,7 @@ class CstTargetSerializer(serializers.Serializer):
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if schema and cst.schema != schema: if schema and cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.id}': msg.constituentaNotInRSform(schema.title) f'{cst.id}': msg.constituentaNotOwned(schema.title)
}) })
if cst.cst_type not in [CstType.FUNCTION, CstType.STRUCTURED, CstType.TERM]: if cst.cst_type not in [CstType.FUNCTION, CstType.STRUCTURED, CstType.TERM]:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -234,7 +234,7 @@ class CstRenameSerializer(serializers.Serializer):
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if cst.schema != schema: if cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.id}': msg.constituentaNotInRSform(schema.title) f'{cst.id}': msg.constituentaNotOwned(schema.title)
}) })
new_alias = self.initial_data['alias'] new_alias = self.initial_data['alias']
if cst.alias == new_alias: if cst.alias == new_alias:
@ -260,7 +260,7 @@ class CstListSerializer(serializers.Serializer):
for item in attrs['items']: for item in attrs['items']:
if item.schema != schema: if item.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{item.id}': msg.constituentaNotInRSform(schema.title) f'{item.id}': msg.constituentaNotOwned(schema.title)
}) })
return attrs return attrs
@ -270,16 +270,17 @@ class CstMoveSerializer(CstListSerializer):
move_to = serializers.IntegerField() move_to = serializers.IntegerField()
class SubstitutionSerializerBase(serializers.Serializer): class CstSubstituteSerializerBase(serializers.Serializer):
''' Serializer: Basic substitution. ''' ''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.all()) original = PKField(many=False, queryset=Constituenta.objects.all())
substitution = PKField(many=False, queryset=Constituenta.objects.all()) substitution = PKField(many=False, queryset=Constituenta.objects.all())
transfer_term = serializers.BooleanField(required=False, default=False)
class CstSubstituteSerializer(serializers.Serializer): class CstSubstituteSerializer(serializers.Serializer):
''' Serializer: Constituenta substitution. ''' ''' Serializer: Constituenta substitution. '''
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionSerializerBase(), child=CstSubstituteSerializerBase(),
min_length=1 min_length=1
) )
@ -299,11 +300,11 @@ class CstSubstituteSerializer(serializers.Serializer):
}) })
if original_cst.schema != schema: if original_cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
'original': msg.constituentaNotInRSform(schema.title) 'original': msg.constituentaNotOwned(schema.title)
}) })
if substitution_cst.schema != schema: if substitution_cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
'substitution': msg.constituentaNotInRSform(schema.title) 'substitution': msg.constituentaNotOwned(schema.title)
}) })
deleted.add(original_cst.pk) deleted.add(original_cst.pk)
return attrs return attrs
@ -315,7 +316,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionSerializerBase() child=CstSubstituteSerializerBase()
) )
def validate(self, attrs): def validate(self, attrs):
@ -324,14 +325,14 @@ class InlineSynthesisSerializer(serializers.Serializer):
schema_out = cast(LibraryItem, attrs['receiver']) schema_out = cast(LibraryItem, attrs['receiver'])
if user.is_anonymous or (schema_out.owner != user and not user.is_staff): if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
raise PermissionDenied({ raise PermissionDenied({
'message': msg.schemaForbidden(), 'message': msg.schemaNotOwned(),
'object_id': schema_in.id 'object_id': schema_in.id
}) })
constituents = cast(list[Constituenta], attrs['items']) constituents = cast(list[Constituenta], attrs['items'])
for cst in constituents: for cst in constituents:
if cst.schema != schema_in: if cst.schema != schema_in:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.id}': msg.constituentaNotInRSform(schema_in.title) f'{cst.id}': msg.constituentaNotOwned(schema_in.title)
}) })
deleted = set() deleted = set()
for item in attrs['substitutions']: for item in attrs['substitutions']:
@ -344,7 +345,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
}) })
if substitution_cst.schema != schema_out: if substitution_cst.schema != schema_out:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{substitution_cst.id}': msg.constituentaNotInRSform(schema_out.title) f'{substitution_cst.id}': msg.constituentaNotOwned(schema_out.title)
}) })
else: else:
if substitution_cst not in constituents: if substitution_cst not in constituents:
@ -353,7 +354,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
}) })
if original_cst.schema != schema_out: if original_cst.schema != schema_out:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{original_cst.id}': msg.constituentaNotInRSform(schema_out.title) f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title)
}) })
if original_cst.pk in deleted: if original_cst.pk in deleted:
raise serializers.ValidationError({ raise serializers.ValidationError({

View File

@ -20,6 +20,12 @@ class TestConstituenta(TestCase):
self.assertEqual(str(cst), testStr) self.assertEqual(str(cst), testStr)
def test_url(self):
testStr = 'X1'
cst = Constituenta.objects.create(alias=testStr, schema=self.schema1.model, order=1, convention='Test')
self.assertEqual(cst.get_absolute_url(), f'/api/constituents/{cst.pk}')
def test_order_not_null(self): def test_order_not_null(self):
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
Constituenta.objects.create(alias='X1', schema=self.schema1.model) Constituenta.objects.create(alias='X1', schema=self.schema1.model)

View File

@ -208,11 +208,11 @@ class TestRSForm(TestCase):
definition_formal=x1.alias definition_formal=x1.alias
) )
self.schema.substitute(x1, x2) self.schema.substitute(x1, x2, True)
x2.refresh_from_db() x2.refresh_from_db()
d1.refresh_from_db() d1.refresh_from_db()
self.assertEqual(self.schema.constituents().count(), 2) self.assertEqual(self.schema.constituents().count(), 2)
self.assertEqual(x2.term_raw, 'Test2') self.assertEqual(x2.term_raw, 'Test')
self.assertEqual(d1.definition_formal, x2.alias) self.assertEqual(d1.definition_formal, x2.alias)

View File

@ -1,4 +1,6 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_cctext import * from .t_cctext import *
from .t_constituents import *
from .t_operations import *
from .t_rsforms import * from .t_rsforms import *
from .t_rslang import * from .t_rslang import *

View File

@ -0,0 +1,102 @@
''' Testing API: Constituents. '''
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestConstituentaAPI(EndpointTester):
''' Testing Constituenta view. '''
def setUp(self):
super().setUp()
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create(
alias='X1',
cst_type=CstType.BASE,
schema=self.rsform_owned.model,
order=1,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
self.cst2 = Constituenta.objects.create(
alias='X2',
cst_type=CstType.BASE,
schema=self.rsform_unowned.model,
order=1,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.cst3 = Constituenta.objects.create(
alias='X3',
schema=self.rsform_owned.model,
order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
self.invalid_cst = self.cst3.pk + 1337
@decl_endpoint('/api/constituents/{item}', method='get')
def test_retrieve(self):
self.executeNotFound(item=self.invalid_cst)
response = self.executeOK(item=self.cst1.pk)
self.assertEqual(response.data['alias'], self.cst1.alias)
self.assertEqual(response.data['convention'], self.cst1.convention)
@decl_endpoint('/api/constituents/{item}', method='patch')
def test_partial_update(self):
data = {'convention': 'tt'}
self.executeForbidden(data=data, item=self.cst2.pk)
self.logout()
self.executeForbidden(data=data, item=self.cst1.pk)
self.login()
response = self.executeOK(data=data, item=self.cst1.pk)
self.cst1.refresh_from_db()
self.assertEqual(response.data['convention'], 'tt')
self.assertEqual(self.cst1.convention, 'tt')
self.executeOK(data=data, item=self.cst1.pk)
@decl_endpoint('/api/constituents/{item}', method='patch')
def test_update_resolved_no_refs(self):
data = {
'term_raw': 'New term',
'definition_raw': 'New def'
}
response = self.executeOK(data=data, item=self.cst3.pk)
self.cst3.refresh_from_db()
self.assertEqual(response.data['term_resolved'], 'New term')
self.assertEqual(self.cst3.term_resolved, 'New term')
self.assertEqual(response.data['definition_resolved'], 'New def')
self.assertEqual(self.cst3.definition_resolved, 'New def')
@decl_endpoint('/api/constituents/{item}', method='patch')
def test_update_resolved_refs(self):
data = {
'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
}
response = self.executeOK(data=data, item=self.cst3.pk)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved)
self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1')
self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1')
@decl_endpoint('/api/constituents/{item}', method='patch')
def test_readonly_cst_fields(self):
data = {'alias': 'X33', 'order': 10}
response = self.executeOK(data=data, item=self.cst1.pk)
self.assertEqual(response.data['alias'], 'X1')
self.assertEqual(response.data['alias'], self.cst1.alias)
self.assertEqual(response.data['order'], self.cst1.order)

View File

@ -0,0 +1,82 @@
''' Testing API: Operations. '''
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestInlineSynthesis(EndpointTester):
''' Testing Operations endpoints. '''
@decl_endpoint('/api/operations/inline-synthesis', method='patch')
def setUp(self):
super().setUp()
self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.create(title='Test3', alias='T3')
def test_inline_synthesis_inputs(self):
invalid_id = 1338
data = {
'receiver': self.unowned.model.pk,
'source': self.schema1.model.pk,
'items': [],
'substitutions': []
}
self.executeForbidden(data=data)
data['receiver'] = invalid_id
self.executeBadData(data=data)
data['receiver'] = self.schema1.model.pk
data['source'] = invalid_id
self.executeBadData(data=data)
data['source'] = self.schema1.model.pk
self.executeOK(data=data)
data['items'] = [invalid_id]
self.executeBadData(data=data)
def test_inline_synthesis(self):
ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete
ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2
ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1
ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1
ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete
ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4
ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2
ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
data = {
'receiver': self.schema1.model.pk,
'source': self.schema2.model.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [
{
'original': ks1_x1.pk,
'substitution': ks2_s1.pk,
'transfer_term': False
},
{
'original': ks2_x1.pk,
'substitution': ks1_s1.pk,
'transfer_term': True
}
]
}
response = self.executeOK(data=data)
result = {item['alias']: item for item in response.data['items']}
self.assertEqual(len(result), 6)
self.assertEqual(result['X2']['term_raw'], ks1_x2.term_raw)
self.assertEqual(result['X2']['order'], 1)
self.assertEqual(result['X4']['term_raw'], ks2_x2.term_raw)
self.assertEqual(result['X4']['order'], 2)
self.assertEqual(result['S1']['term_raw'], ks2_x1.term_raw)
self.assertEqual(result['S2']['term_raw'], ks2_s1.term_raw)
self.assertEqual(result['S1']['definition_formal'], 'X2')
self.assertEqual(result['S2']['definition_formal'], 'X4×X4')
self.assertEqual(result['D1']['definition_formal'], r'S1\S2\X2')
self.assertEqual(result['D2']['definition_formal'], r'S2\S1\X4')

View File

@ -272,6 +272,43 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x1.cst_type, CstType.TERM) self.assertEqual(x1.cst_type, CstType.TERM)
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_single(self):
x1 = self.owned.insert_new(
alias='X1',
term_raw='Test1',
term_resolved='Test1',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}]
)
x2 = self.owned.insert_new(
alias='X2',
term_raw='Test2'
)
unowned = self.unowned.insert_new('X2')
data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]}
self.executeForbidden(data=data, item=self.unowned_id)
self.executeBadData(data=data, item=self.owned_id)
data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.executeBadData(data=data, item=self.owned_id)
data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.executeBadData(data=data, item=self.owned_id)
d1 = self.owned.insert_new(
alias='D1',
term_raw='@{X2|sing,datv}',
definition_formal='X1'
)
data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]}
response = self.executeOK(data=data, item=self.owned_id)
d1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(x2.term_raw, 'Test1')
self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2')
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch') @decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_multiple(self): def test_substitute_multiple(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
@ -290,11 +327,13 @@ class TestRSFormViewset(EndpointTester):
data = {'substitutions': [ data = {'substitutions': [
{ {
'original': x1.pk, 'original': x1.pk,
'substitution': d1.pk 'substitution': d1.pk,
'transfer_term': True
}, },
{ {
'original': x1.pk, 'original': x1.pk,
'substitution': d2.pk 'substitution': d2.pk,
'transfer_term': True
} }
]} ]}
self.executeBadData(data=data) self.executeBadData(data=data)
@ -302,11 +341,13 @@ class TestRSFormViewset(EndpointTester):
data = {'substitutions': [ data = {'substitutions': [
{ {
'original': x1.pk, 'original': x1.pk,
'substitution': d1.pk 'substitution': d1.pk,
'transfer_term': True
}, },
{ {
'original': x2.pk, 'original': x2.pk,
'substitution': d2.pk 'substitution': d2.pk,
'transfer_term': True
} }
]} ]}
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
@ -482,172 +523,3 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(len(items), 2) self.assertEqual(len(items), 2)
self.assertEqual(items[0]['order'], f1.order + 1) self.assertEqual(items[0]['order'], f1.order + 1)
self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])') self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])')
class TestConstituentaAPI(EndpointTester):
''' Testing Constituenta view. '''
def setUp(self):
super().setUp()
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create(
alias='X1',
cst_type=CstType.BASE,
schema=self.rsform_owned.model,
order=1,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
self.cst2 = Constituenta.objects.create(
alias='X2',
cst_type=CstType.BASE,
schema=self.rsform_unowned.model,
order=1,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.cst3 = Constituenta.objects.create(
alias='X3',
schema=self.rsform_owned.model,
order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
self.invalid_cst = self.cst3.pk + 1337
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_partial_update(self):
data = {'id': self.cst1.pk, 'convention': 'tt'}
self.executeForbidden(data=data, schema=self.rsform_unowned.model.pk)
self.logout()
self.executeForbidden(data=data, schema=self.rsform_owned.model.pk)
self.login()
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst1.refresh_from_db()
self.assertEqual(response.data['convention'], 'tt')
self.assertEqual(self.cst1.convention, 'tt')
self.executeOK(data=data, schema=self.rsform_owned.model.pk)
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_no_refs(self):
data = {
'id': self.cst3.pk,
'term_raw': 'New term',
'definition_raw': 'New def'
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst3.refresh_from_db()
self.assertEqual(response.data['term_resolved'], 'New term')
self.assertEqual(self.cst3.term_resolved, 'New term')
self.assertEqual(response.data['definition_resolved'], 'New def')
self.assertEqual(self.cst3.definition_resolved, 'New def')
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_refs(self):
data = {
'id': self.cst3.pk,
'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
self.assertEqual(response.data['term_resolved'], self.cst1.term_resolved)
self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1')
self.assertEqual(response.data['definition_resolved'], f'{self.cst1.term_resolved} form1')
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_readonly_cst_fields(self):
data = {
'id': self.cst1.pk,
'alias': 'X33',
'order': 10
}
response = self.executeOK(data=data, schema=self.rsform_owned.model.pk)
self.assertEqual(response.data['alias'], 'X1')
self.assertEqual(response.data['alias'], self.cst1.alias)
self.assertEqual(response.data['order'], self.cst1.order)
class TestInlineSynthesis(EndpointTester):
''' Testing Operations endpoints. '''
@decl_endpoint('/api/rsforms/inline-synthesis', method='patch')
def setUp(self):
super().setUp()
self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.create(title='Test3', alias='T3')
def test_inline_synthesis_inputs(self):
invalid_id = 1338
data = {
'receiver': self.unowned.model.pk,
'source': self.schema1.model.pk,
'items': [],
'substitutions': []
}
self.executeForbidden(data=data)
data['receiver'] = invalid_id
self.executeBadData(data=data)
data['receiver'] = self.schema1.model.pk
data['source'] = invalid_id
self.executeBadData(data=data)
data['source'] = self.schema1.model.pk
self.executeOK(data=data)
data['items'] = [invalid_id]
self.executeBadData(data=data)
def test_inline_synthesis(self):
ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete
ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2
ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1
ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1
ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete
ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4
ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2
ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
data = {
'receiver': self.schema1.model.pk,
'source': self.schema2.model.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [
{
'original': ks1_x1.pk,
'substitution': ks2_s1.pk
},
{
'original': ks2_x1.pk,
'substitution': ks1_s1.pk
}
]
}
response = self.executeOK(data=data)
result = {item['alias']: item for item in response.data['items']}
self.assertEqual(len(result), 6)
self.assertEqual(result['X2']['order'], 1)
self.assertEqual(result['X4']['order'], 2)
self.assertEqual(result['S1']['definition_formal'], 'X2')
self.assertEqual(result['S2']['definition_formal'], 'X4×X4')
self.assertEqual(result['D1']['definition_formal'], r'S1\S2\X2')
self.assertEqual(result['D2']['definition_formal'], r'S2\S1\X4')

View File

@ -9,9 +9,11 @@ library_router.register('rsforms', views.RSFormViewSet, 'RSForm')
urlpatterns = [ urlpatterns = [
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
path('rsforms/import-trs', views.TrsImportView.as_view()), path('rsforms/import-trs', views.TrsImportView.as_view()),
path('rsforms/create-detailed', views.create_rsform), path('rsforms/create-detailed', views.create_rsform),
path('rsforms/inline-synthesis', views.inline_synthesis),
path('operations/inline-synthesis', views.inline_synthesis),
path('rslang/parse-expression', views.parse_expression), path('rslang/parse-expression', views.parse_expression),
path('rslang/to-ascii', views.convert_to_ascii), path('rslang/to-ascii', views.convert_to_ascii),

View File

@ -1,4 +1,6 @@
''' REST API: Endpoint processors. ''' ''' REST API: Endpoint processors. '''
from .cctext import generate_lexeme, inflect, parse_text from .cctext import generate_lexeme, inflect, parse_text
from .rsforms import RSFormViewSet, TrsImportView, create_rsform, inline_synthesis from .constituents import ConstituentAPIView
from .operations import inline_synthesis
from .rsforms import RSFormViewSet, TrsImportView, create_rsform
from .rslang import convert_to_ascii, convert_to_math, parse_expression from .rslang import convert_to_ascii, convert_to_math, parse_expression

View File

@ -0,0 +1,16 @@
''' Endpoints for Constituenta. '''
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics
from shared import permissions
from .. import models as m
from .. import serializers as s
@extend_schema(tags=['Constituenta'])
@extend_schema_view()
class ConstituentAPIView(generics.RetrieveUpdateAPIView, permissions.EditorMixin):
''' Endpoint: Get / Update Constituenta. '''
queryset = m.Constituenta.objects.all()
serializer_class = s.CstSerializer

View File

@ -0,0 +1,50 @@
''' Endpoints for RSForm. '''
from typing import cast
from django.db import transaction
from drf_spectacular.utils import extend_schema
from rest_framework import status as c
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response
from .. import models as m
from .. import serializers as s
@extend_schema(
summary='Inline synthesis: merge one schema into another',
tags=['Operations'],
request=s.InlineSynthesisSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@api_view(['PATCH'])
def inline_synthesis(request: Request):
''' Endpoint: Inline synthesis. '''
serializer = s.InlineSynthesisSerializer(
data=request.data,
context={'user': request.user}
)
serializer.is_valid(raise_exception=True)
receiver = m.RSForm(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic():
new_items = receiver.insert_copy(items)
for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution'])
if original in items:
index = next(i for (i, cst) in enumerate(items) if cst == original)
original = new_items[index]
else:
index = next(i for (i, cst) in enumerate(items) if cst == replacement)
replacement = new_items[index]
receiver.substitute(original, replacement, substitution['transfer_term'])
receiver.restore_order()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(receiver.model).data
)

View File

@ -12,7 +12,6 @@ from rest_framework import views, viewsets
from rest_framework.decorators import action, api_view from rest_framework.decorators import action, api_view
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
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
@ -46,8 +45,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'substitute', 'substitute',
'restore_order', 'restore_order',
'reset_aliases', 'reset_aliases',
'produce_structure', 'produce_structure'
'update_cst'
]: ]:
permission_list = [permissions.ItemEditor] permission_list = [permissions.ItemEditor]
elif self.action in [ elif self.action in [
@ -90,43 +88,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
new_cst = m.RSForm(schema).create_cst(data, insert_after) new_cst = m.RSForm(schema).create_cst(data, insert_after)
schema.refresh_from_db() schema.refresh_from_db()
return Response( response = 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).data
} }
) )
response['Location'] = new_cst.get_absolute_url()
@extend_schema( return response
summary='update persistent attributes of a given constituenta',
tags=['RSForm'],
request=s.CstSerializer,
responses={
c.HTTP_200_OK: s.CstSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='update-cst')
def update_cst(self, request: Request, pk):
''' Update persistent attributes of a given constituenta. '''
schema = self._get_item()
serializer = s.CstSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
cst = m.Constituenta.objects.get(pk=request.data['id'])
if cst.schema != schema:
raise ValidationError({
'schema': msg.constituentaNotInRSform(schema.title)
})
serializer.update(instance=cst, validated_data=serializer.validated_data)
return Response(
status=c.HTTP_200_OK,
data=s.CstSerializer(cst).data
)
@extend_schema( @extend_schema(
summary='produce the structure of a given constituenta', summary='produce the structure of a given constituenta',
@ -204,7 +174,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
) )
@extend_schema( @extend_schema(
summary='execute substitutions', summary='substitute constituenta',
tags=['RSForm'], tags=['RSForm'],
request=s.CstSubstituteSerializer, request=s.CstSubstituteSerializer,
responses={ responses={
@ -228,7 +198,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
m.RSForm(schema).substitute(original, replacement) m.RSForm(schema).substitute(original, replacement, substitution['transfer_term'])
schema.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
@ -551,41 +521,3 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC) data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC)
data['location'] = request.data.get('location', LocationHead.USER) data['location'] = request.data.get('location', LocationHead.USER)
@extend_schema(
summary='Inline synthesis: merge one schema into another',
tags=['Operations'],
request=s.InlineSynthesisSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer}
)
@api_view(['PATCH'])
def inline_synthesis(request: Request):
''' Endpoint: Inline synthesis. '''
serializer = s.InlineSynthesisSerializer(
data=request.data,
context={'user': request.user}
)
serializer.is_valid(raise_exception=True)
receiver = m.RSForm(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic():
new_items = receiver.insert_copy(items)
for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution'])
if original in items:
index = next(i for (i, cst) in enumerate(items) if cst == original)
original = new_items[index]
else:
index = next(i for (i, cst) in enumerate(items) if cst == replacement)
replacement = new_items[index]
receiver.substitute(original, replacement)
receiver.restore_order()
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(receiver.model).data
)

View File

@ -2,27 +2,19 @@
# pylint: skip-file # pylint: skip-file
def constituentaNotInRSform(title: str): def constituentaNotOwned(title: str):
return f'Конституента не принадлежит схеме: {title}' return f'Конституента не принадлежит схеме: {title}'
def constituentaNotFromOperation(): def operationNotOwned(title: str):
return f'Конституента не соответствую аргументам операции' return f'Операция не принадлежит схеме: {title}'
def operationNotInOSS(title: str):
return f'Операция не принадлежит ОСС: {title}'
def previousResultMissing():
return 'Отсутствует результат предыдущей операции'
def substitutionNotInList(): def substitutionNotInList():
return 'Отождествляемая конституента отсутствует в списке' return 'Отождествляемая конституента отсутствует в списке'
def schemaForbidden(): def schemaNotOwned():
return 'Нет доступа к схеме' return 'Нет доступа к схеме'
@ -30,10 +22,6 @@ def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}' return f'Операция не является Загрузкой: {title}'
def operationNotSynthesis(title: str):
return f'Операция не является Синтезом: {title}'
def operationResultNotEmpty(title: str): def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}' return f'Результат операции не пуст: {title}'

View File

@ -32,24 +32,6 @@ def _extract_item(obj: Any) -> LibraryItem:
}) })
def can_edit_item(user, obj: Any) -> bool:
if user.is_anonymous:
return False
if hasattr(user, 'is_staff') and user.is_staff:
return True
item = _extract_item(obj)
if item.owner == user:
return True
if Editor.objects.filter(
item=item,
editor=cast(User, user)
).exists() and item.access_policy != AccessPolicy.PRIVATE:
return True
return False
class GlobalAdmin(_Base): class GlobalAdmin(_Base):
''' Item permission: Admin or higher. ''' ''' Item permission: Admin or higher. '''

View File

@ -0,0 +1,14 @@
/**
* Endpoints: constituents.
*/
import { IConstituentaMeta, ICstUpdateData } from '@/models/rsform';
import { AxiosPatch, FrontExchange } from './apiTransport';
export function patchConstituenta(target: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
endpoint: `/api/constituents/${target}`,
request: request
});
}

View File

@ -0,0 +1,14 @@
/**
* Endpoints: operations.
*/
import { IInlineSynthesisData, IRSFormData } from '@/models/rsform';
import { AxiosPatch, FrontExchange } from './apiTransport';
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/operations/inline-synthesis`,
request: request
});
}

View File

@ -7,8 +7,6 @@ import {
IOperationCreateData, IOperationCreateData,
IOperationCreatedResponse, IOperationCreatedResponse,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData,
IOperationUpdateData,
IPositionsData, IPositionsData,
ITargetOperation ITargetOperation
} from '@/models/oss'; } from '@/models/oss';
@ -52,24 +50,3 @@ export function patchCreateInput(oss: string, request: FrontExchange<ITargetOper
request: request request: request
}); });
} }
export function patchSetInput(oss: string, request: FrontExchange<IOperationSetInputData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/set-input`,
request: request
});
}
export function patchUpdateOperation(oss: string, request: FrontExchange<IOperationUpdateData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/update-operation`,
request: request
});
}
export function postExecuteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPost({
endpoint: `/api/oss/${oss}/execute-operation`,
request: request
});
}

View File

@ -6,13 +6,10 @@ import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss'; import { ICstSubstituteData } from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
IConstituentaMeta,
ICstCreateData, ICstCreateData,
ICstCreatedResponse, ICstCreatedResponse,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstUpdateData,
IInlineSynthesisData,
IProduceStructureResponse, IProduceStructureResponse,
IRSFormData, IRSFormData,
IRSFormUploadData, IRSFormUploadData,
@ -71,13 +68,6 @@ export function postCreateConstituenta(schema: string, request: FrontExchange<IC
}); });
} }
export function patchUpdateConstituenta(schema: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/update-cst`,
request: request
});
}
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) { export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/rsforms/${schema}/delete-multiple-cst`, endpoint: `/api/rsforms/${schema}/delete-multiple-cst`,
@ -145,10 +135,3 @@ export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUpl
} }
}); });
} }
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/inline-synthesis`,
request: request
});
}

View File

@ -89,6 +89,8 @@ export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi'; export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi';
export { LuPower as IconKeepAliasOn } from 'react-icons/lu'; export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu'; export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
export { LuFlag as IconKeepTermOn } from 'react-icons/lu';
export { LuFlagOff as IconKeepTermOff } from 'react-icons/lu';
// ===== Domain actions ===== // ===== Domain actions =====
export { BiUpvote as IconMoveUp } from 'react-icons/bi'; export { BiUpvote as IconMoveUp } from 'react-icons/bi';
@ -106,7 +108,7 @@ export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWand2 as IconGenerateNames } from 'react-icons/lu'; export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr'; export { GrConnect as IconConnect } from 'react-icons/gr';
export { BiPlayCircle as IconExecute } from 'react-icons/bi'; export { BsPlay as IconExecute } from 'react-icons/bs';
// ======== Graph UI ======= // ======== Graph UI =======
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi'; export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';

View File

@ -1,126 +1,105 @@
'use client'; 'use client';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import BadgeConstituenta from '@/components/info/BadgeConstituenta'; import BadgeConstituenta from '@/components/info/BadgeConstituenta';
import SelectConstituenta from '@/components/select/SelectConstituenta'; import SelectConstituenta from '@/components/select/SelectConstituenta';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { ILibraryItem } from '@/models/library'; import { IConstituenta, IRSForm, ISingleSubstitution } from '@/models/rsform';
import { ICstSubstitute, IMultiSubstitution } from '@/models/oss'; import { describeConstituenta } from '@/utils/labels';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { errors } from '@/utils/labels';
import { IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons'; import {
IconKeepAliasOff,
IconKeepAliasOn,
IconKeepTermOff,
IconKeepTermOn,
IconPageFirst,
IconPageLast,
IconPageLeft,
IconPageRight,
IconRemove,
IconReplace
} from '../Icons';
import NoData from '../ui/NoData'; import NoData from '../ui/NoData';
import SelectLibraryItem from './SelectLibraryItem';
interface PickSubstitutionsProps { interface PickSubstitutionsProps {
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
prefixID: string; prefixID: string;
rows?: number; rows?: number;
allowSelfSubstitution?: boolean;
schemas: IRSForm[]; schema1?: IRSForm;
filter?: (cst: IConstituenta) => boolean; schema2?: IRSForm;
filter1?: (cst: IConstituenta) => boolean;
filter2?: (cst: IConstituenta) => boolean;
items: ISingleSubstitution[];
setItems: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
const columnHelper = createColumnHelper<IMultiSubstitution>(); function SubstitutionIcon({ item }: { item: ISingleSubstitution }) {
if (item.deleteRight) {
if (item.takeLeftTerm) {
return <IconPageRight size='1.2rem' />;
} else {
return <IconPageLast size='1.2rem' />;
}
} else {
if (item.takeLeftTerm) {
return <IconPageFirst size='1.2rem' />;
} else {
return <IconPageLeft size='1.2rem' />;
}
}
}
const columnHelper = createColumnHelper<ISingleSubstitution>();
function PickSubstitutions({ function PickSubstitutions({
substitutions, items,
setSubstitutions, schema1,
prefixID, schema2,
filter1,
filter2,
rows, rows,
schemas, setItems,
filter, prefixID
allowSelfSubstitution
}: PickSubstitutionsProps) { }: PickSubstitutionsProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
const [leftArgument, setLeftArgument] = useState<ILibraryItem | undefined>(
schemas.length === 1 ? schemas[0] : undefined
);
const [rightArgument, setRightArgument] = useState<ILibraryItem | undefined>(
schemas.length === 1 && allowSelfSubstitution ? schemas[0] : undefined
);
const [leftCst, setLeftCst] = useState<IConstituenta | undefined>(undefined); const [leftCst, setLeftCst] = useState<IConstituenta | undefined>(undefined);
const [rightCst, setRightCst] = useState<IConstituenta | undefined>(undefined); const [rightCst, setRightCst] = useState<IConstituenta | undefined>(undefined);
const [deleteRight, setDeleteRight] = useState(true); const [deleteRight, setDeleteRight] = useState(true);
const [takeLeftTerm, setTakeLeftTerm] = useState(true);
const toggleDelete = () => setDeleteRight(prev => !prev); const toggleDelete = () => setDeleteRight(prev => !prev);
const toggleTerm = () => setTakeLeftTerm(prev => !prev);
const getSchemaByCst = useCallback(
(id: ConstituentaID): IRSForm | undefined => {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return schema;
}
}
return undefined;
},
[schemas]
);
const getConstituenta = useCallback(
(id: ConstituentaID): IConstituenta | undefined => {
for (const schema of schemas) {
const cst = schema.cstByID.get(id);
if (cst) {
return cst;
}
}
return undefined;
},
[schemas]
);
const substitutionData: IMultiSubstitution[] = useMemo(
() =>
substitutions.map(item => ({
original_source: getSchemaByCst(item.original),
original: getConstituenta(item.original),
substitution: getConstituenta(item.substitution),
substitution_source: getSchemaByCst(item.substitution)
})),
[getConstituenta, getSchemaByCst, substitutions]
);
function addSubstitution() { function addSubstitution() {
if (!leftCst || !rightCst) { if (!leftCst || !rightCst) {
return; return;
} }
const newSubstitution: ICstSubstitute = { const newSubstitution: ISingleSubstitution = {
original: deleteRight ? rightCst.id : leftCst.id, leftCst: leftCst,
substitution: deleteRight ? leftCst.id : rightCst.id rightCst: rightCst,
deleteRight: deleteRight,
takeLeftTerm: takeLeftTerm
}; };
const toDelete = substitutions.map(item => item.original); setItems([
const replacements = substitutions.map(item => item.substitution); newSubstitution,
console.log(toDelete, replacements); ...items.filter(
console.log(newSubstitution); item =>
if ( (!item.deleteRight && item.leftCst.id !== leftCst.id) ||
toDelete.includes(newSubstitution.original) || (item.deleteRight && item.rightCst.id !== rightCst.id)
toDelete.includes(newSubstitution.substitution) || )
replacements.includes(newSubstitution.original) ]);
) {
toast.error(errors.reuseOriginal);
return;
}
setSubstitutions(prev => [...prev, newSubstitution]);
setLeftCst(undefined);
setRightCst(undefined);
} }
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(row: number) => { (row: number) => {
setSubstitutions(prev => { setItems(prev => {
const newItems: ICstSubstitute[] = []; const newItems: ISingleSubstitution[] = [];
prev.forEach((item, index) => { prev.forEach((item, index) => {
if (index !== row) { if (index !== row) {
newItems.push(item); newItems.push(item);
@ -129,62 +108,54 @@ function PickSubstitutions({
return newItems; return newItems;
}); });
}, },
[setSubstitutions] [setItems]
); );
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', { columnHelper.accessor(item => describeConstituenta(item.leftCst), {
id: 'left_schema', id: 'left_text',
header: 'Операция', header: 'Описание',
size: 100, size: 1000,
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-right'>{props.getValue()}</div> cell: props => <div className='text-xs text-ellipsis'>{props.getValue()}</div>
}), }),
columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', { columnHelper.accessor(item => item.leftCst.alias, {
id: 'left_alias', id: 'left_alias',
header: () => <span className='pl-3'>Имя</span>, header: () => <span className='pl-3'>Имя</span>,
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.substitution ? ( <BadgeConstituenta theme={colors} value={props.row.original.leftCst} prefixID={`${prefixID}_1_`} />
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.display({ columnHelper.display({
id: 'status', id: 'status',
header: '', header: '',
size: 40, size: 40,
cell: () => <IconPageRight size='1.2rem' /> cell: props => <SubstitutionIcon item={props.row.original} />
}), }),
columnHelper.accessor(item => item.original?.alias ?? 'N/A', { columnHelper.accessor(item => item.rightCst.alias, {
id: 'right_alias', id: 'right_alias',
header: () => <span className='pl-3'>Имя</span>, header: () => <span className='pl-3'>Имя</span>,
size: 65, size: 65,
cell: props => cell: props => (
props.row.original.original ? ( <BadgeConstituenta theme={colors} value={props.row.original.rightCst} prefixID={`${prefixID}_2_`} />
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} /> )
) : (
'N/A'
)
}), }),
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', { columnHelper.accessor(item => describeConstituenta(item.rightCst), {
id: 'right_schema', id: 'right_text',
header: 'Операция', header: 'Описание',
size: 100, minSize: 1000,
cell: props => <div className='min-w-[8rem] text-ellipsis'>{props.getValue()}</div> cell: props => <div className='text-xs text-ellipsis text-pretty'>{props.getValue()}</div>
}), }),
columnHelper.display({ columnHelper.display({
id: 'actions', id: 'actions',
cell: props => ( cell: props => (
<div className='max-w-fit'> <MiniButton
<MiniButton noHover
noHover title='Удалить'
title='Удалить' icon={<IconRemove size='1rem' className='icon-red' />}
icon={<IconRemove size='1rem' className='icon-red' />} onClick={() => handleDeleteRow(props.row.index)}
onClick={() => handleDeleteRow(props.row.index)} />
/>
</div>
) )
}) })
], ],
@ -194,65 +165,87 @@ function PickSubstitutions({
return ( return (
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<div className='flex items-end gap-3 justify-stretch'> <div className='flex items-end gap-3 justify-stretch'>
<div className='flex-grow flex flex-col basis-1/2'> <div className='flex-grow basis-1/2'>
<div className='flex flex-col gap-[0.125rem] border-x border-t clr-input'> <div className='flex items-center justify-between'>
<SelectLibraryItem <Label text={schema1 !== schema2 ? schema1?.alias ?? 'Схема 1' : ''} />
noBorder <div className='cc-icons'>
placeholder='Выберите аргумент' <MiniButton
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)} title='Сохранить конституенту'
value={leftArgument} noHover
onSelectValue={setLeftArgument} onClick={toggleDelete}
/> icon={
<SelectConstituenta deleteRight ? (
noBorder <IconKeepAliasOn size='1rem' className='clr-text-green' />
items={(leftArgument as IRSForm)?.items.filter( ) : (
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst)) <IconKeepAliasOff size='1rem' className='clr-text-red' />
)} )
value={leftCst} }
onSelectValue={setLeftCst} />
/> <MiniButton
title='Сохранить термин'
noHover
onClick={toggleTerm}
icon={
takeLeftTerm ? (
<IconKeepTermOn size='1rem' className='clr-text-green' />
) : (
<IconKeepTermOff size='1rem' className='clr-text-red' />
)
}
/>
</div>
</div> </div>
</div> <SelectConstituenta
<div className='flex flex-col gap-1'> items={schema1?.items.filter(cst => !filter1 || filter1(cst))}
<MiniButton value={leftCst}
title={deleteRight ? 'Заменить правую' : 'Заменить левую'} onSelectValue={setLeftCst}
onClick={toggleDelete}
icon={
deleteRight ? (
<IconPageRight size='1.5rem' className='clr-text-primary' />
) : (
<IconPageLeft size='1.5rem' className='clr-text-primary' />
)
}
/> />
</div>
<MiniButton <MiniButton
title='Добавить в таблицу отождествлений' noHover
className='mb-[0.375rem] grow-0' title='Добавить в таблицу отождествлений'
icon={<IconReplace size='1.5rem' className='icon-primary' />} className='mb-[0.375rem] grow-0'
disabled={!leftCst || !rightCst || leftCst === rightCst} icon={<IconReplace size='1.5rem' className='icon-primary' />}
onClick={addSubstitution} disabled={!leftCst || !rightCst || leftCst === rightCst}
/> onClick={addSubstitution}
</div> />
<div className='flex-grow basis-1/2'> <div className='flex-grow basis-1/2'>
<div className='flex flex-col gap-[0.125rem] border-x border-t clr-input'> <div className='flex items-center justify-between'>
<SelectLibraryItem <Label text={schema1 !== schema2 ? schema2?.alias ?? 'Схема 2' : ''} />
noBorder <div className='cc-icons'>
placeholder='Выберите аргумент' <MiniButton
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)} title='Сохранить конституенту'
value={rightArgument} noHover
onSelectValue={setRightArgument} onClick={toggleDelete}
/> icon={
<SelectConstituenta !deleteRight ? (
noBorder <IconKeepAliasOn size='1rem' className='clr-text-green' />
items={(rightArgument as IRSForm)?.items.filter( ) : (
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst)) <IconKeepAliasOff size='1rem' className='clr-text-red' />
)} )
value={rightCst} }
onSelectValue={setRightCst} />
/> <MiniButton
title='Сохранить термин'
noHover
onClick={toggleTerm}
icon={
!takeLeftTerm ? (
<IconKeepTermOn size='1rem' className='clr-text-green' />
) : (
<IconKeepTermOff size='1rem' className='clr-text-red' />
)
}
/>
</div>
</div> </div>
<SelectConstituenta
items={schema2?.items.filter(cst => !filter2 || filter2(cst))}
value={rightCst}
onSelectValue={setRightCst}
/>
</div> </div>
</div> </div>
@ -263,7 +256,7 @@ function PickSubstitutions({
className='w-full text-sm border select-none cc-scroll-y' className='w-full text-sm border select-none cc-scroll-y'
rows={rows} rows={rows}
contentHeight='1.3rem' contentHeight='1.3rem'
data={substitutionData} data={items}
columns={columns} columns={columns}
headPosition='0' headPosition='0'
noDataComponent={ noDataComponent={

View File

@ -49,7 +49,7 @@ function SelectConstituenta({
<SelectSingle <SelectSingle
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : undefined}
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
// @ts-expect-error: TODO: use type definitions from react-select in filter object // @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filter}

View File

@ -1,60 +0,0 @@
'use client';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI';
import { CProps } from '../props';
import SelectSingle from '../ui/SelectSingle';
interface SelectLibraryItemProps extends CProps.Styling {
items?: ILibraryItem[];
value?: ILibraryItem;
onSelectValue: (newValue?: ILibraryItem) => void;
placeholder?: string;
noBorder?: boolean;
}
function SelectLibraryItem({
className,
items,
value,
onSelectValue,
placeholder = 'Выберите схему',
...restProps
}: SelectLibraryItemProps) {
const options = useMemo(() => {
return (
items?.map(cst => ({
value: cst.id,
label: `${cst.alias}: ${cst.title}`
})) ?? []
);
}, [items]);
const filter = useCallback(
(option: { value: LibraryItemID | undefined; label: string }, inputValue: string) => {
const item = items?.find(item => item.id === option.value);
return !item ? false : matchLibraryItem(item, inputValue);
},
[items]
);
return (
<SelectSingle
className={clsx('text-ellipsis', className)}
options={options}
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null}
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
// @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter}
placeholder={placeholder}
{...restProps}
/>
);
}
export default SelectLibraryItem;

View File

@ -47,7 +47,7 @@ function SelectOperation({
<SelectSingle <SelectSingle
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : null} value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : undefined}
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))} onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
// @ts-expect-error: TODO: use type definitions from react-select in filter object // @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filter}

View File

@ -49,7 +49,7 @@ function SelectUser({
<SelectSingle <SelectSingle
className={clsx('text-ellipsis', className)} className={clsx('text-ellipsis', className)}
options={options} options={options}
value={value ? { value: value, label: getUserLabel(value) } : null} value={value ? { value: value, label: getUserLabel(value) } : undefined}
onChange={data => { onChange={data => {
if (data !== null && data.value !== undefined) onSelectValue(data.value); if (data !== null && data.value !== undefined) onSelectValue(data.value);
}} }}

View File

@ -12,15 +12,7 @@ import {
patchSetOwner, patchSetOwner,
postSubscribe postSubscribe
} from '@/backend/library'; } from '@/backend/library';
import { import { patchCreateInput, patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss';
patchCreateInput,
patchDeleteOperation,
patchSetInput,
patchUpdateOperation,
patchUpdatePositions,
postCreateOperation,
postExecuteOperation
} from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
@ -29,8 +21,6 @@ import {
IOperationCreateData, IOperationCreateData,
IOperationSchema, IOperationSchema,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData,
IOperationUpdateData,
IPositionsData, IPositionsData,
ITargetOperation ITargetOperation
} from '@/models/oss'; } from '@/models/oss';
@ -65,9 +55,6 @@ interface IOssContext {
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void; createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
} }
const OssContext = createContext<IOssContext | null>(null); const OssContext = createContext<IOssContext | null>(null);
@ -346,71 +333,6 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, library] [itemID, library]
); );
const setInput = useCallback(
(data: IOperationSetInputData, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchSetInput(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.localUpdateTimestamp(newData.id);
if (callback) callback();
}
});
},
[itemID, schema, library]
);
const updateOperation = useCallback(
(data: IOperationUpdateData, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchUpdateOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.reloadItems(() => {
if (callback) callback();
});
}
});
},
[itemID, schema, library]
);
const executeOperation = useCallback(
(data: ITargetOperation, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
postExecuteOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.reloadItems(() => {
if (callback) callback();
});
}
});
},
[itemID, schema, library]
);
return ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
@ -434,10 +356,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
savePositions, savePositions,
createOperation, createOperation,
deleteOperation, deleteOperation,
createInput, createInput
setInput,
updateOperation,
executeOperation
}} }}
> >
{children} {children}

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { patchConstituenta } from '@/backend/constituents';
import { import {
deleteUnsubscribe, deleteUnsubscribe,
patchLibraryItem, patchLibraryItem,
@ -13,17 +14,16 @@ import {
postCreateVersion, postCreateVersion,
postSubscribe postSubscribe
} from '@/backend/library'; } from '@/backend/library';
import { patchInlineSynthesis } from '@/backend/operations';
import { import {
getTRSFile, getTRSFile,
patchDeleteConstituenta, patchDeleteConstituenta,
patchInlineSynthesis,
patchMoveConstituenta, patchMoveConstituenta,
patchProduceStructure, patchProduceStructure,
patchRenameConstituenta, patchRenameConstituenta,
patchResetAliases, patchResetAliases,
patchRestoreOrder, patchRestoreOrder,
patchSubstituteConstituents, patchSubstituteConstituents,
patchUpdateConstituenta,
patchUploadTRS, patchUploadTRS,
postCreateConstituenta postCreateConstituenta
} from '@/backend/rsforms'; } from '@/backend/rsforms';
@ -157,11 +157,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
onSuccess: newData => { onSuccess: newData => {
setSchema(Object.assign(schema, newData)); setSchema(Object.assign(schema, newData));
library.localUpdateItem(newData); library.localUpdateItem(newData);
if (library.globalOSS?.schemas.includes(newData.id)) { if (callback) callback(newData);
library.reloadOSS(() => {
if (callback) callback(newData);
});
} else if (callback) callback(newData);
} }
}); });
}, },
@ -439,7 +435,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
const cstUpdate = useCallback( const cstUpdate = useCallback(
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => { (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined); setProcessingError(undefined);
patchUpdateConstituenta(itemID, { patchConstituenta(String(data.id), {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,

View File

@ -1,71 +0,0 @@
'use client';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema';
import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import Modal, { ModalProps } from '@/components/ui/Modal';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperation, IOperationSchema } from '@/models/oss';
interface DlgChangeInputSchemaProps extends Pick<ModalProps, 'hideWindow'> {
oss: IOperationSchema;
target: IOperation;
onSubmit: (newSchema: LibraryItemID | undefined) => void;
}
function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) {
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const baseFilter = useCallback(
(item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result,
[oss, selected, target]
);
const isValid = useMemo(() => target.result !== selected, [target, selected]);
const handleSelectLocation = useCallback((newValue: LibraryItemID) => {
setSelected(newValue);
}, []);
function handleSubmit() {
onSubmit(selected);
}
return (
<Modal
overflowVisible
header='Выбор концептуальной схемы'
submitText='Подтвердить выбор'
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')}
>
<div className='flex justify-between gap-3 items-center'>
<div className='flex gap-3'>
<Label text='Загружаемая концептуальная схема' />
<MiniButton
title='Сбросить выбор схемы'
noHover
noPadding
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => setSelected(undefined)}
disabled={selected == undefined}
/>
</div>
</div>
<PickSchema
value={selected} // prettier: split-line
onSelectValue={handleSelectLocation}
rows={8}
baseFilter={baseFilter}
/>
</Modal>
);
}
export default DlgChangeInputSchema;

View File

@ -10,8 +10,8 @@ import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic, Position2D } from '@/models/miscellaneous';
import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss'; import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { describeOperationType, labelOperationType } from '@/utils/labels'; import { describeOperationType, labelOperationType } from '@/utils/labels';
@ -21,6 +21,8 @@ import TabSynthesisOperation from './TabSynthesisOperation';
interface DlgCreateOperationProps { interface DlgCreateOperationProps {
hideWindow: () => void; hideWindow: () => void;
oss: IOperationSchema; oss: IOperationSchema;
positions: IOperationPosition[];
insertPosition: Position2D;
onCreate: (data: IOperationCreateData) => void; onCreate: (data: IOperationCreateData) => void;
} }
@ -29,7 +31,7 @@ export enum TabID {
SYNTHESIS = 1 SYNTHESIS = 1
} }
function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationProps) { function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCreate }: DlgCreateOperationProps) {
const library = useLibrary(); const library = useLibrary();
const [activeTab, setActiveTab] = useState(TabID.INPUT); const [activeTab, setActiveTab] = useState(TabID.INPUT);
@ -38,6 +40,7 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [inputs, setInputs] = useState<OperationID[]>([]); const [inputs, setInputs] = useState<OperationID[]>([]);
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined); const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
const [syncText, setSyncText] = useState(true);
const [createSchema, setCreateSchema] = useState(false); const [createSchema, setCreateSchema] = useState(false);
const isValid = useMemo( const isValid = useMemo(
@ -59,15 +62,16 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
const handleSubmit = () => { const handleSubmit = () => {
const data: IOperationCreateData = { const data: IOperationCreateData = {
item_data: { item_data: {
position_x: 0, position_x: insertPosition.x,
position_y: 0, position_y: insertPosition.y,
alias: alias, alias: alias,
title: title, title: title,
comment: comment, comment: comment,
sync_text: activeTab === TabID.INPUT ? syncText : true,
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS, operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
result: activeTab === TabID.INPUT ? attachedID ?? null : null result: activeTab === TabID.INPUT ? attachedID ?? null : null
}, },
positions: [], positions: positions,
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined, arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined,
create_schema: createSchema create_schema: createSchema
}; };
@ -87,12 +91,14 @@ function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationPro
setTitle={setTitle} setTitle={setTitle}
attachedID={attachedID} attachedID={attachedID}
setAttachedID={setAttachedID} setAttachedID={setAttachedID}
syncText={syncText}
setSyncText={setSyncText}
createSchema={createSchema} createSchema={createSchema}
setCreateSchema={setCreateSchema} setCreateSchema={setCreateSchema}
/> />
</TabPanel> </TabPanel>
), ),
[alias, comment, title, attachedID, oss, createSchema] [alias, comment, title, attachedID, syncText, oss, createSchema]
); );
const synthesisPanel = useMemo( const synthesisPanel = useMemo(

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect } from 'react';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
@ -24,6 +25,8 @@ interface TabInputOperationProps {
setComment: React.Dispatch<React.SetStateAction<string>>; setComment: React.Dispatch<React.SetStateAction<string>>;
attachedID: LibraryItemID | undefined; attachedID: LibraryItemID | undefined;
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>; setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
syncText: boolean;
setSyncText: React.Dispatch<React.SetStateAction<boolean>>;
createSchema: boolean; createSchema: boolean;
setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>; setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>;
} }
@ -38,6 +41,8 @@ function TabInputOperation({
setComment, setComment,
attachedID, attachedID,
setAttachedID, setAttachedID,
syncText,
setSyncText,
createSchema, createSchema,
setCreateSchema setCreateSchema
}: TabInputOperationProps) { }: TabInputOperationProps) {
@ -46,8 +51,9 @@ function TabInputOperation({
useEffect(() => { useEffect(() => {
if (createSchema) { if (createSchema) {
setAttachedID(undefined); setAttachedID(undefined);
setSyncText(true);
} }
}, [createSchema, setAttachedID]); }, [createSchema, setAttachedID, setSyncText]);
return ( return (
<AnimateFade className='cc-column'> <AnimateFade className='cc-column'>
@ -56,19 +62,27 @@ function TabInputOperation({
label='Полное название' label='Полное название'
value={title} value={title}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
disabled={attachedID !== undefined} disabled={syncText && attachedID !== undefined}
/> />
<div className='flex gap-6'> <div className='flex gap-6'>
<TextInput <FlexColumn>
id='operation_alias' <TextInput
label='Сокращение' id='operation_alias'
className='w-[14rem]' label='Сокращение'
pattern={patterns.library_alias} className='w-[14rem]'
title={`не более ${limits.library_alias_len} символов`} pattern={patterns.library_alias}
value={alias} title={`не более ${limits.library_alias_len} символов`}
onChange={event => setAlias(event.target.value)} value={alias}
disabled={attachedID !== undefined} onChange={event => setAlias(event.target.value)}
/> disabled={syncText && attachedID !== undefined}
/>
<Checkbox
value={syncText}
setValue={setSyncText}
label='Синхронизировать текст'
title='Брать текст из концептуальной схемы'
/>
</FlexColumn>
<TextArea <TextArea
id='operation_comment' id='operation_comment'
@ -77,7 +91,7 @@ function TabInputOperation({
rows={3} rows={3}
value={comment} value={comment}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
disabled={attachedID !== undefined} disabled={syncText && attachedID !== undefined}
/> />
</div> </div>

View File

@ -11,9 +11,9 @@ import { prefixes } from '@/utils/constants';
import ListConstituents from './ListConstituents'; import ListConstituents from './ListConstituents';
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> { interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
schema: IRSForm;
selected: ConstituentaID[]; selected: ConstituentaID[];
onDelete: (items: ConstituentaID[]) => void; onDelete: (items: ConstituentaID[]) => void;
schema: IRSForm;
} }
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) { function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {

View File

@ -1,164 +0,0 @@
'use client';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import BadgeHelp from '@/components/info/BadgeHelp';
import Modal from '@/components/ui/Modal';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import useRSFormCache from '@/hooks/useRSFormCache';
import { HelpTopic } from '@/models/miscellaneous';
import {
ICstSubstitute,
IOperation,
IOperationSchema,
IOperationUpdateData,
OperationID,
OperationType
} from '@/models/oss';
import { PARAMETER } from '@/utils/constants';
import TabArguments from './TabArguments';
import TabOperation from './TabOperation';
import TabSynthesis from './TabSynthesis';
interface DlgEditOperationProps {
hideWindow: () => void;
oss: IOperationSchema;
target: IOperation;
onSubmit: (data: IOperationUpdateData) => void;
}
export enum TabID {
CARD = 0,
ARGUMENTS = 1,
SUBSTITUTION = 2
}
function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperationProps) {
const [activeTab, setActiveTab] = useState(TabID.CARD);
const [alias, setAlias] = useState(target.alias);
const [title, setTitle] = useState(target.title);
const [comment, setComment] = useState(target.comment);
const [inputs, setInputs] = useState<OperationID[]>(oss.graph.expandInputs([target.id]));
const inputOperations = useMemo(() => inputs.map(id => oss.operationByID.get(id)!), [inputs, oss.operationByID]);
const schemasIDs = useMemo(
() => inputOperations.map(operation => operation.result).filter(id => id !== null),
[inputOperations]
);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>(oss.substitutions);
const cache = useRSFormCache();
const schemas = useMemo(
() => schemasIDs.map(id => cache.getSchema(id)).filter(item => item !== undefined),
[schemasIDs, cache]
);
const isValid = useMemo(() => alias !== '', [alias]);
useEffect(() => {
cache.preload(schemasIDs);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [schemasIDs]);
const handleSubmit = () => {
const data: IOperationUpdateData = {
target: target.id,
item_data: {
alias: alias,
title: title,
comment: comment
},
positions: [],
arguments: target.operation_type !== OperationType.SYNTHESIS ? undefined : inputs,
substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
};
onSubmit(data);
};
const cardPanel = useMemo(
() => (
<TabPanel>
<TabOperation
alias={alias}
setAlias={setAlias}
comment={comment}
setComment={setComment}
title={title}
setTitle={setTitle}
/>
</TabPanel>
),
[alias, comment, title]
);
const argumentsPanel = useMemo(
() => (
<TabPanel>
<TabArguments
target={target.id} // prettier: split-lines
oss={oss}
inputs={inputs}
setInputs={setInputs}
/>
</TabPanel>
),
[oss, target, inputs]
);
const synthesisPanel = useMemo(
() => (
<TabPanel>
<TabSynthesis
schemas={schemas}
loading={cache.loading}
error={cache.error}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</TabPanel>
),
[cache.loading, cache.error, substitutions, schemas]
);
return (
<Modal
header='Редактирование операции'
submitText='Сохранить'
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
className='w-[40rem] px-6 min-h-[35rem]'
>
<Overlay position='top-0 right-0'>
<BadgeHelp topic={HelpTopic.CC_OSS} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} offset={14} />
</Overlay>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
<TabLabel title='Текстовые поля' label='Карточка' className='w-[8rem]' />
{target.operation_type === OperationType.SYNTHESIS ? (
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-[8rem]' />
) : null}
{target.operation_type === OperationType.SYNTHESIS ? (
<TabLabel title='Таблица отождествлений' label='Отождествления' className='w-[8rem]' />
) : null}
</TabList>
{cardPanel}
{target.operation_type === OperationType.SYNTHESIS ? argumentsPanel : null}
{target.operation_type === OperationType.SYNTHESIS ? synthesisPanel : null}
</Tabs>
</Modal>
);
}
export default DlgEditOperation;

View File

@ -1,35 +0,0 @@
'use client';
import { useMemo } from 'react';
import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label';
import AnimateFade from '@/components/wrap/AnimateFade';
import { IOperationSchema, OperationID } from '@/models/oss';
import PickMultiOperation from '../../components/select/PickMultiOperation';
interface TabArgumentsProps {
oss: IOperationSchema;
target: OperationID;
inputs: OperationID[];
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
}
function TabArguments({ oss, inputs, target, setInputs }: TabArgumentsProps) {
const potentialCycle = useMemo(() => [target, ...oss.graph.expandAllOutputs([target])], [target, oss.graph]);
const filtered = useMemo(
() => oss.items.filter(item => !potentialCycle.includes(item.id)),
[oss.items, potentialCycle]
);
return (
<AnimateFade className='cc-column'>
<FlexColumn>
<Label text={`Выбор аргументов: [ ${inputs.length} ]`} />
<PickMultiOperation items={filtered} selected={inputs} setSelected={setInputs} rows={8} />
</FlexColumn>
</AnimateFade>
);
}
export default TabArguments;

View File

@ -1,48 +0,0 @@
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import AnimateFade from '@/components/wrap/AnimateFade';
import { limits, patterns } from '@/utils/constants';
interface TabOperationProps {
alias: string;
setAlias: React.Dispatch<React.SetStateAction<string>>;
title: string;
setTitle: React.Dispatch<React.SetStateAction<string>>;
comment: string;
setComment: React.Dispatch<React.SetStateAction<string>>;
}
function TabOperation({ alias, setAlias, title, setTitle, comment, setComment }: TabOperationProps) {
return (
<AnimateFade className='cc-column'>
<TextInput
id='operation_title'
label='Полное название'
value={title}
onChange={event => setTitle(event.target.value)}
/>
<div className='flex gap-6'>
<TextInput
id='operation_alias'
label='Сокращение'
className='w-[14rem]'
pattern={patterns.library_alias}
title={`не более ${limits.library_alias_len} символов`}
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<TextArea
id='operation_comment'
label='Описание'
noResize
rows={3}
value={comment}
onChange={event => setComment(event.target.value)}
/>
</div>
</AnimateFade>
);
}
export default TabOperation;

View File

@ -1,31 +0,0 @@
import { ErrorData } from '@/components/info/InfoError';
import PickSubstitutions from '@/components/select/PickSubstitutions';
import DataLoader from '@/components/wrap/DataLoader';
import { ICstSubstitute } from '@/models/oss';
import { IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface TabSynthesisProps {
loading: boolean;
error: ErrorData;
schemas: IRSForm[];
substitutions: ICstSubstitute[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
}
function TabSynthesis({ schemas, loading, error, substitutions, setSubstitutions }: TabSynthesisProps) {
return (
<DataLoader id='dlg-synthesis-tab' className='cc-column mt-3' isLoading={loading} error={error}>
<PickSubstitutions
schemas={schemas}
prefixID={prefixes.dlg_cst_substitutes_list}
rows={8}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</DataLoader>
);
}
export default TabSynthesis;

View File

@ -1 +0,0 @@
export { default } from './DlgEditOperation';

View File

@ -8,8 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstSubstitute } from '@/models/oss'; import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform';
import { IInlineSynthesisData, IRSForm } from '@/models/rsform';
import TabConstituents from './TabConstituents'; import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema'; import TabSchema from './TabSchema';
@ -31,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined); const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<LibraryItemID[]>([]); const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]); const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });
@ -45,7 +44,11 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
source: source.schema?.id, source: source.schema?.id,
receiver: receiver.id, receiver: receiver.id,
items: selected, items: selected,
substitutions: substitutions substitutions: substitutions.map(item => ({
original: item.deleteRight ? item.rightCst.id : item.leftCst.id,
substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id,
transfer_term: !item.deleteRight && item.takeLeftTerm
}))
}; };
onInlineSynthesis(data); onInlineSynthesis(data);
} }

View File

@ -1,14 +1,12 @@
'use client'; 'use client';
import { useCallback, useMemo } from 'react';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import PickSubstitutions from '@/components/select/PickSubstitutions';
import DataLoader from '@/components/wrap/DataLoader'; import DataLoader from '@/components/wrap/DataLoader';
import { ICstSubstitute } from '@/models/oss'; import { ConstituentaID, IRSForm, ISingleSubstitution } from '@/models/rsform';
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import PickSubstitutions from '../../components/select/PickSubstitutions';
interface TabSubstitutionsProps { interface TabSubstitutionsProps {
receiver?: IRSForm; receiver?: IRSForm;
source?: IRSForm; source?: IRSForm;
@ -17,8 +15,8 @@ interface TabSubstitutionsProps {
loading?: boolean; loading?: boolean;
error?: ErrorData; error?: ErrorData;
substitutions: ICstSubstitute[]; substitutions: ISingleSubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
function TabSubstitutions({ function TabSubstitutions({
@ -32,22 +30,16 @@ function TabSubstitutions({
substitutions, substitutions,
setSubstitutions setSubstitutions
}: TabSubstitutionsProps) { }: TabSubstitutionsProps) {
const filter = useCallback(
(cst: IConstituenta) => cst.id !== source?.id || selected.includes(cst.id),
[selected, source]
);
const schemas = useMemo(() => [...(source ? [source] : []), ...(receiver ? [receiver] : [])], [source, receiver]);
return ( return (
<DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={loading} error={error} hasNoData={!source}> <DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={loading} error={error} hasNoData={!source}>
<PickSubstitutions <PickSubstitutions
substitutions={substitutions} items={substitutions}
setSubstitutions={setSubstitutions} setItems={setSubstitutions}
rows={10} rows={10}
prefixID={prefixes.cst_inline_synth_substitutes} prefixID={prefixes.cst_inline_synth_substitutes}
schemas={schemas} schema1={receiver}
filter={filter} schema2={source}
filter2={cst => selected.includes(cst.id)}
/> />
</DataLoader> </DataLoader>
); );

View File

@ -5,23 +5,29 @@ import { useMemo, useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions'; import PickSubstitutions from '@/components/select/PickSubstitutions';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { ICstSubstitute, ICstSubstituteData } from '@/models/oss'; import { useRSForm } from '@/context/RSFormContext';
import { IRSForm } from '@/models/rsform'; import { ICstSubstituteData } from '@/models/oss';
import { ISingleSubstitution } from '@/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> { interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
schema: IRSForm;
onSubstitute: (data: ICstSubstituteData) => void; onSubstitute: (data: ICstSubstituteData) => void;
} }
function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCstProps) { function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]); const { schema } = useRSForm();
const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]); const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);
function handleSubmit() { function handleSubmit() {
const data: ICstSubstituteData = { const data: ICstSubstituteData = {
substitutions: substitutions substitutions: substitutions.map(item => ({
original: item.deleteRight ? item.rightCst.id : item.leftCst.id,
substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id,
transfer_term: !item.deleteRight && item.takeLeftTerm
}))
}; };
onSubstitute(data); onSubstitute(data);
} }
@ -37,12 +43,12 @@ function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCst
className={clsx('w-[40rem]', 'px-6 pb-3')} className={clsx('w-[40rem]', 'px-6 pb-3')}
> >
<PickSubstitutions <PickSubstitutions
allowSelfSubstitution items={substitutions}
substitutions={substitutions} setItems={setSubstitutions}
setSubstitutions={setSubstitutions}
rows={6} rows={6}
prefixID={prefixes.dlg_cst_substitutes_list} prefixID={prefixes.dlg_cst_substitutes_list}
schemas={[schema]} schema1={schema}
schema2={schema}
/> />
</Modal> </Modal>
); );

View File

@ -1,82 +0,0 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { getRSFormDetails } from '@/backend/rsforms';
import { type ErrorData } from '@/components/info/InfoError';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID, IRSForm, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader';
function useRSFormCache() {
const [cache, setCache] = useState<IRSForm[]>([]);
const [pending, setPending] = useState<LibraryItemID[]>([]);
const [processing, setProcessing] = useState<LibraryItemID[]>([]);
const loading = useMemo(() => pending.length > 0 || processing.length > 0, [pending, processing]);
const [error, setError] = useState<ErrorData>(undefined);
function setSchema(data: IRSFormData) {
const schema = new RSFormLoader(data).produceRSForm();
setCache(prev => [...prev, schema]);
}
const getSchema = useCallback((id: LibraryItemID) => cache.find(item => item.id === id), [cache]);
const getSchemaByCst = useCallback(
(id: ConstituentaID) => {
for (const schema of cache) {
const cst = schema.items.find(cst => cst.id === id);
if (cst) {
return schema;
}
}
return undefined;
},
[cache]
);
const getConstituenta = useCallback(
(id: ConstituentaID) => {
for (const schema of cache) {
const cst = schema.items.find(cst => cst.id === id);
if (cst) {
return cst;
}
}
return undefined;
},
[cache]
);
const preload = useCallback(
(target: LibraryItemID[]) => setPending(prev => [...prev, ...target.filter(id => !prev.includes(id))]),
[]
);
useEffect(() => {
const ids = pending.filter(id => !processing.includes(id) && !cache.find(schema => schema.id === id));
if (ids.length === 0) {
return;
}
setProcessing(prev => [...prev, ...ids]);
setPending([]);
ids.forEach(id =>
getRSFormDetails(String(id), '', {
showError: false,
onError: error => {
setProcessing(prev => prev.filter(item => item !== id));
setError(error);
},
onSuccess: data => {
setProcessing(prev => prev.filter(item => item !== id));
setSchema(data);
}
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pending]);
return { preload, getSchema, getConstituenta, getSchemaByCst, loading, error, setError };
}
export default useRSFormCache;

View File

@ -4,7 +4,7 @@
import { Graph } from './Graph'; import { Graph } from './Graph';
import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library';
import { ConstituentaID, IConstituenta } from './rsform'; import { ConstituentaID } from './rsform';
/** /**
* Represents {@link IOperation} identifier type. * Represents {@link IOperation} identifier type.
@ -30,6 +30,7 @@ export interface IOperation {
alias: string; alias: string;
title: string; title: string;
comment: string; comment: string;
sync_text: boolean;
position_x: number; position_x: number;
position_y: number; position_y: number;
@ -62,28 +63,12 @@ export interface ITargetOperation extends IPositionsData {
export interface IOperationCreateData extends IPositionsData { export interface IOperationCreateData extends IPositionsData {
item_data: Pick< item_data: Pick<
IOperation, IOperation,
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' 'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' | 'sync_text'
>; >;
arguments: OperationID[] | undefined; arguments: OperationID[] | undefined;
create_schema: boolean; create_schema: boolean;
} }
/**
* Represents {@link IOperation} data, used in update process.
*/
export interface IOperationUpdateData extends ITargetOperation {
item_data: Pick<IOperation, 'alias' | 'title' | 'comment'>;
arguments: OperationID[] | undefined;
substitutions: ICstSubstitute[] | undefined;
}
/**
* Represents {@link IOperation} data, used in setInput process.
*/
export interface IOperationSetInputData extends ITargetOperation {
input: LibraryItemID | null;
}
/** /**
* Represents {@link IOperation} Argument. * Represents {@link IOperation} Argument.
*/ */
@ -98,6 +83,7 @@ export interface IArgument {
export interface ICstSubstitute { export interface ICstSubstitute {
original: ConstituentaID; original: ConstituentaID;
substitution: ConstituentaID; substitution: ConstituentaID;
transfer_term: boolean;
} }
/** /**
@ -107,16 +93,6 @@ export interface ICstSubstituteData {
substitutions: ICstSubstitute[]; substitutions: ICstSubstitute[];
} }
/**
* Represents substitution for multi synthesis table.
*/
export interface IMultiSubstitution {
original_source: ILibraryItem | undefined;
original: IConstituenta | undefined;
substitution: IConstituenta | undefined;
substitution_source: ILibraryItem | undefined;
}
/** /**
* Represents {@link ICstSubstitute} extended data. * Represents {@link ICstSubstitute} extended data.
*/ */

View File

@ -241,12 +241,13 @@ export interface IVersionCreatedResponse {
} }
/** /**
* Represents single substitution for binary synthesis table. * Represents single substitution for synthesis table.
*/ */
export interface IBinarySubstitution { export interface ISingleSubstitution {
leftCst: IConstituenta; leftCst: IConstituenta;
rightCst: IConstituenta; rightCst: IConstituenta;
deleteRight: boolean; deleteRight: boolean;
takeLeftTerm: boolean;
} }
/** /**

View File

@ -4,6 +4,8 @@ import {
IconGenerateNames, IconGenerateNames,
IconGenerateStructure, IconGenerateStructure,
IconInlineSynthesis, IconInlineSynthesis,
IconKeepAliasOn,
IconKeepTermOn,
IconReplace, IconReplace,
IconSortList, IconSortList,
IconTemplates IconTemplates
@ -58,7 +60,13 @@ function HelpRSLangOperations() {
</h2> </h2>
<p> <p>
Формирование таблицы отождествлений и ее применение к текущей схеме. В результате будет удален ряд конституент и Формирование таблицы отождествлений и ее применение к текущей схеме. В результате будет удален ряд конституент и
их вхождения заменены на другие. их вхождения заменены на другие. Возможна настройка какой термин использовать для оставшихся конституент
<li>
<IconKeepAliasOn size='1.25rem' className='inline-icon' /> выбор сохраняемой конституенты
</li>
<li>
<IconKeepTermOn size='1.25rem' className='inline-icon' /> выбор сохраняемого термина
</li>
</p> </p>
<h2> <h2>

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons'; import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
@ -22,45 +23,12 @@ interface NodeContextMenuProps extends ContextMenuData {
onHide: () => void; onHide: () => void;
onDelete: (target: OperationID) => void; onDelete: (target: OperationID) => void;
onCreateInput: (target: OperationID) => void; onCreateInput: (target: OperationID) => void;
onEditSchema: (target: OperationID) => void;
onEditOperation: (target: OperationID) => void;
onExecuteOperation: (target: OperationID) => void;
} }
function NodeContextMenu({ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCreateInput }: NodeContextMenuProps) {
operation,
cursorX,
cursorY,
onHide,
onDelete,
onCreateInput,
onEditSchema,
onEditOperation,
onExecuteOperation
}: NodeContextMenuProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null); const ref = useRef(null);
const readyForSynthesis = useMemo(() => {
if (operation.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (!controller.schema || operation.result) {
return false;
}
const argumentIDs = controller.schema.graph.expandInputs([operation.id]);
if (!argumentIDs || argumentIDs.length < 2) {
return false;
}
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
return true;
}, [operation, controller.schema]);
const handleHide = useCallback(() => { const handleHide = useCallback(() => {
setIsOpen(false); setIsOpen(false);
@ -76,13 +44,13 @@ function NodeContextMenu({
}; };
const handleEditSchema = () => { const handleEditSchema = () => {
toast.error('Not implemented');
handleHide(); handleHide();
onEditSchema(operation.id);
}; };
const handleEditOperation = () => { const handleEditOperation = () => {
toast.error('Not implemented');
handleHide(); handleHide();
onEditOperation(operation.id);
}; };
const handleDeleteOperation = () => { const handleDeleteOperation = () => {
@ -96,8 +64,8 @@ function NodeContextMenu({
}; };
const handleRunSynthesis = () => { const handleRunSynthesis = () => {
toast.error('Not implemented');
handleHide(); handleHide();
onExecuteOperation(operation.id);
}; };
return ( return (
@ -129,9 +97,9 @@ function NodeContextMenu({
onClick={handleCreateSchema} onClick={handleCreateSchema}
/> />
) : null} ) : null}
{controller.isMutable && operation.operation_type === OperationType.INPUT ? ( {controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? (
<DropdownButton <DropdownButton
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'} text='Загрузить схему'
title='Выбрать схему для загрузки' title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />} icon={<IconConnect size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
@ -141,13 +109,9 @@ function NodeContextMenu({
{controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? ( {controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton <DropdownButton
text='Выполнить синтез' text='Выполнить синтез'
title={ title='Выполнить операцию и получить синтезированную КС'
readyForSynthesis
? 'Выполнить операцию и получить синтезированную КС'
: 'Необходимо предоставить все аргументы'
}
icon={<IconExecute size='1rem' className='icon-green' />} icon={<IconExecute size='1rem' className='icon-green' />}
disabled={controller.isProcessing || !readyForSynthesis} disabled={controller.isProcessing}
onClick={handleRunSynthesis} onClick={handleRunSynthesis}
/> />
) : null} ) : null}

View File

@ -28,7 +28,9 @@ function OperationNode(node: OssNodeInternal) {
noHover noHover
title='Связанная КС' title='Связанная КС'
hideTitle={!controller.showTooltip} hideTitle={!controller.showTooltip}
onClick={handleOpenSchema} onClick={() => {
handleOpenSchema();
}}
disabled={!hasFile} disabled={!hasFile}
/> />
</Overlay> </Overlay>

View File

@ -148,34 +148,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
[controller, getPositions] [controller, getPositions]
); );
const handleEditSchema = useCallback(
(target: OperationID) => {
controller.promptEditInput(target, getPositions());
},
[controller, getPositions]
);
const handleEditOperation = useCallback(
(target: OperationID) => {
controller.promptEditOperation(target, getPositions());
},
[controller, getPositions]
);
const handleExecuteOperation = useCallback(
(target: OperationID) => {
controller.executeOperation(target, getPositions());
},
[controller, getPositions]
);
const handleExecuteSelected = useCallback(() => {
if (controller.selected.length !== 1) {
return;
}
handleExecuteOperation(controller.selected[0]);
}, [controller, handleExecuteOperation]);
const handleFitView = useCallback(() => { const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration }); flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]); }, [flow]);
@ -241,17 +213,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleContextMenuHide(); handleContextMenuHide();
}, [handleContextMenuHide]); }, [handleContextMenuHide]);
const handleNodeClick = useCallback(
(event: CProps.EventMouse, node: OssNode) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
event.stopPropagation();
handleEditOperation(Number(node.id));
}
},
[handleEditOperation]
);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) { if (controller.isProcessing) {
return; return;
@ -265,12 +226,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleSavePositions(); handleSavePositions();
return; return;
} }
if ((event.ctrlKey || event.metaKey) && event.key === 'q') {
event.preventDefault();
event.stopPropagation();
handleCreateOperation();
return;
}
if (event.key === 'Delete') { if (event.key === 'Delete') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -297,9 +252,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
edges={edges} edges={edges}
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick}
proOptions={{ hideAttribution: true }}
fitView fitView
proOptions={{ hideAttribution: true }}
nodeTypes={OssNodeTypes} nodeTypes={OssNodeTypes}
maxZoom={2} maxZoom={2}
minZoom={0.75} minZoom={0.75}
@ -312,17 +266,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
{showGrid ? <Background gap={10} /> : null} {showGrid ? <Background gap={10} /> : null}
</ReactFlow> </ReactFlow>
), ),
[ [nodes, edges, handleNodesChange, handleContextMenu, handleClickCanvas, onEdgesChange, OssNodeTypes, showGrid]
nodes,
edges,
handleNodesChange,
handleContextMenu,
handleClickCanvas,
onEdgesChange,
handleNodeClick,
OssNodeTypes,
showGrid
]
); );
return ( return (
@ -336,8 +280,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
onFitView={handleFitView} onFitView={handleFitView}
onCreate={handleCreateOperation} onCreate={handleCreateOperation}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onEdit={() => handleEditOperation(controller.selected[0])}
onExecute={handleExecuteSelected}
onResetPositions={handleResetPositions} onResetPositions={handleResetPositions}
onSavePositions={handleSavePositions} onSavePositions={handleSavePositions}
onSaveImage={handleSaveImage} onSaveImage={handleSaveImage}
@ -351,9 +293,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
onHide={handleContextMenuHide} onHide={handleContextMenuHide}
onDelete={handleDeleteOperation} onDelete={handleDeleteOperation}
onCreateInput={handleCreateInput} onCreateInput={handleCreateInput}
onEditSchema={handleEditSchema}
onEditOperation={handleEditOperation}
onExecuteOperation={handleExecuteOperation}
{...menuProps} {...menuProps}
/> />
) : null} ) : null}

View File

@ -1,14 +1,9 @@
'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react';
import { import {
IconAnimation, IconAnimation,
IconAnimationOff, IconAnimationOff,
IconDestroy, IconDestroy,
IconEdit2,
IconExecute,
IconFitImage, IconFitImage,
IconGrid, IconGrid,
IconImage, IconImage,
@ -21,7 +16,6 @@ import {
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { OperationType } from '@/models/oss';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/labels'; import { prepareTooltip } from '@/utils/labels';
@ -34,8 +28,6 @@ interface ToolbarOssGraphProps {
edgeStraight: boolean; edgeStraight: boolean;
onCreate: () => void; onCreate: () => void;
onDelete: () => void; onDelete: () => void;
onEdit: () => void;
onExecute: () => void;
onFitView: () => void; onFitView: () => void;
onSaveImage: () => void; onSaveImage: () => void;
onSavePositions: () => void; onSavePositions: () => void;
@ -52,8 +44,6 @@ function ToolbarOssGraph({
edgeStraight, edgeStraight,
onCreate, onCreate,
onDelete, onDelete,
onEdit,
onExecute,
onFitView, onFitView,
onSaveImage, onSaveImage,
onSavePositions, onSavePositions,
@ -63,40 +53,10 @@ function ToolbarOssGraph({
toggleEdgeStraight toggleEdgeStraight
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const selectedOperation = useMemo(
() => controller.schema?.operationByID.get(controller.selected[0]),
[controller.selected, controller.schema]
);
const readyForSynthesis = useMemo(() => {
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (!controller.schema || selectedOperation.result) {
return false;
}
const argumentIDs = controller.schema.graph.expandInputs([selectedOperation.id]);
if (!argumentIDs || argumentIDs.length < 2) {
return false;
}
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
if (argumentOperations.some(item => item.result === null)) {
return false;
}
return true;
}, [selectedOperation, controller.schema]);
return ( return (
<div className='flex flex-col items-center'> <div className='flex flex-col items-center'>
<div className='cc-icons'> <div className='cc-icons'>
<MiniButton
title='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isModified}
onClick={onResetPositions}
/>
<MiniButton <MiniButton
icon={<IconFitImage size='1.25rem' className='icon-primary' />} icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Сбросить вид' title='Сбросить вид'
@ -148,6 +108,7 @@ function ToolbarOssGraph({
</div> </div>
{controller.isMutable ? ( {controller.isMutable ? (
<div className='cc-icons'> <div className='cc-icons'>
{' '}
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}
@ -155,25 +116,19 @@ function ToolbarOssGraph({
onClick={onSavePositions} onClick={onSavePositions}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')} title='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isModified}
onClick={onResetPositions}
/>
<MiniButton
title='Новая операция'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={onCreate} onClick={onCreate}
/> />
<MiniButton <MiniButton
title='Выполнить операцию' title='Удалить выбранную'
icon={<IconExecute size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
onClick={onExecute}
/>
<MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Ctrl + клик')}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
disabled={controller.selected.length !== 1 || controller.isProcessing}
onClick={onEdit}
/>
<MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={controller.selected.length !== 1 || controller.isProcessing} disabled={controller.selected.length !== 1 || controller.isProcessing}
onClick={onDelete} onClick={onDelete}
@ -183,5 +138,5 @@ function ToolbarOssGraph({
</div> </div>
); );
} }
//IconExecute
export default ToolbarOssGraph; export default ToolbarOssGraph;

View File

@ -10,21 +10,12 @@ import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditEditors from '@/dialogs/DlgEditEditors';
import DlgEditOperation from '@/dialogs/DlgEditOperation'; import { AccessPolicy } from '@/models/library';
import { AccessPolicy, LibraryItemID } from '@/models/library';
import { Position2D } from '@/models/miscellaneous'; import { Position2D } from '@/models/miscellaneous';
import { import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID } from '@/models/oss';
IOperationCreateData,
IOperationPosition,
IOperationSchema,
IOperationSetInputData,
IOperationUpdateData,
OperationID
} from '@/models/oss';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
@ -54,9 +45,6 @@ export interface IOssEditContext {
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void; promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -91,17 +79,10 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const [showEditEditors, setShowEditEditors] = useState(false); const [showEditEditors, setShowEditEditors] = useState(false);
const [showEditLocation, setShowEditLocation] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false);
const [showEditInput, setShowEditInput] = useState(false);
const [showEditOperation, setShowEditOperation] = useState(false);
const [showCreateOperation, setShowCreateOperation] = useState(false); const [showCreateOperation, setShowCreateOperation] = useState(false);
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 }); const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
const [positions, setPositions] = useState<IOperationPosition[]>([]); const [positions, setPositions] = useState<IOperationPosition[]>([]);
const [targetOperationID, setTargetOperationID] = useState<OperationID | undefined>(undefined);
const targetOperation = useMemo(
() => (targetOperationID ? model.schema?.operationByID.get(targetOperationID) : undefined),
[model, targetOperationID]
);
useLayoutEffect( useLayoutEffect(
() => () =>
@ -216,26 +197,9 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
const handleCreateOperation = useCallback( const handleCreateOperation = useCallback(
(data: IOperationCreateData) => { (data: IOperationCreateData) => {
data.positions = positions;
data.item_data.position_x = insertPosition.x;
data.item_data.position_y = insertPosition.y;
model.createOperation(data, operation => toast.success(information.newOperation(operation.alias))); model.createOperation(data, operation => toast.success(information.newOperation(operation.alias)));
}, },
[model, positions, insertPosition] [model]
);
const promptEditOperation = useCallback((target: OperationID, positions: IOperationPosition[]) => {
setPositions(positions);
setTargetOperationID(target);
setShowEditOperation(true);
}, []);
const handleEditOperation = useCallback(
(data: IOperationUpdateData) => {
data.positions = positions;
model.updateOperation(data, () => toast.success(information.changesSaved));
},
[model, positions]
); );
const deleteOperation = useCallback( const deleteOperation = useCallback(
@ -257,38 +221,6 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model, router] [model, router]
); );
const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => {
setPositions(positions);
setTargetOperationID(target);
setShowEditInput(true);
}, []);
const setTargetInput = useCallback(
(newInput: LibraryItemID | undefined) => {
if (!targetOperationID) {
return;
}
const data: IOperationSetInputData = {
target: targetOperationID,
positions: positions,
input: newInput ?? null
};
model.setInput(data, () => toast.success(information.changesSaved));
},
[model, targetOperationID, positions]
);
const executeOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
const data = {
target: target,
positions: positions
};
model.executeOperation(data, () => toast.success(information.operationExecuted));
},
[model]
);
return ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
@ -314,10 +246,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
savePositions, savePositions,
promptCreateOperation, promptCreateOperation,
deleteOperation, deleteOperation,
createInput, createInput
promptEditInput,
promptEditOperation,
executeOperation
}} }}
> >
{model.schema ? ( {model.schema ? (
@ -340,25 +269,11 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
<DlgCreateOperation <DlgCreateOperation
hideWindow={() => setShowCreateOperation(false)} hideWindow={() => setShowCreateOperation(false)}
oss={model.schema} oss={model.schema}
positions={positions}
insertPosition={insertPosition}
onCreate={handleCreateOperation} onCreate={handleCreateOperation}
/> />
) : null} ) : null}
{showEditInput ? (
<DlgChangeInputSchema
hideWindow={() => setShowEditInput(false)}
oss={model.schema}
target={targetOperation!}
onSubmit={setTargetInput}
/>
) : null}
{showEditOperation ? (
<DlgEditOperation
hideWindow={() => setShowEditOperation(false)}
oss={model.schema}
target={targetOperation!}
onSubmit={handleEditOperation}
/>
) : null}
</AnimatePresence> </AnimatePresence>
) : null} ) : null}

View File

@ -646,7 +646,6 @@ export const RSEditState = ({
) : null} ) : null}
{showSubstitute ? ( {showSubstitute ? (
<DlgSubstituteCst <DlgSubstituteCst
schema={model.schema}
hideWindow={() => setShowSubstitute(false)} // prettier: split lines hideWindow={() => setShowSubstitute(false)} // prettier: split lines
onSubstitute={handleSubstituteCst} onSubstitute={handleSubstituteCst}
/> />

View File

@ -940,8 +940,6 @@ export const information = {
versionDestroyed: 'Версия удалена', versionDestroyed: 'Версия удалена',
itemDestroyed: 'Схема удалена', itemDestroyed: 'Схема удалена',
operationDestroyed: 'Операция удалена', operationDestroyed: 'Операция удалена',
operationExecuted: 'Операция выполнена',
allOperationExecuted: 'Все операции выполнены',
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}` constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
}; };
@ -951,8 +949,7 @@ export const information = {
export const errors = { export const errors = {
astFailed: 'Невозможно построить дерево разбора', astFailed: 'Невозможно построить дерево разбора',
passwordsMismatch: 'Пароли не совпадают', passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения', imageFailed: 'Ошибка при создании изображения'
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении'
}; };
/** /**