Compare commits
7 Commits
01c0eb201e
...
2a30661355
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2a30661355 | ||
![]() |
6a21125e87 | ||
![]() |
2e19c6fa69 | ||
![]() |
afd3f5f7e4 | ||
![]() |
0a7cfa1375 | ||
![]() |
d3213211b5 | ||
![]() |
336b61957b |
|
@ -432,7 +432,8 @@ disable=too-many-public-methods,
|
|||
missing-function-docstring,
|
||||
attribute-defined-outside-init,
|
||||
ungrouped-imports,
|
||||
abstract-method
|
||||
abstract-method,
|
||||
fixme
|
||||
|
||||
# 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
|
||||
|
|
|
@ -140,7 +140,7 @@ class LibraryItem(Model):
|
|||
def _update_connected_operations(self):
|
||||
# using method level import to prevent circular dependency
|
||||
from apps.oss.models import Operation # pylint: disable=import-outside-toplevel
|
||||
operations = Operation.objects.filter(result__pk=self.pk, sync_text=True)
|
||||
operations = Operation.objects.filter(result__pk=self.pk)
|
||||
if not operations.exists():
|
||||
return
|
||||
for operation in operations:
|
||||
|
|
|
@ -21,7 +21,7 @@ class ArgumentAdmin(admin.ModelAdmin):
|
|||
class SynthesisSubstitutionAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Substitutions as part of Synthesis operation. '''
|
||||
ordering = ['operation']
|
||||
list_display = ['id', 'operation', 'original', 'substitution', 'transfer_term']
|
||||
list_display = ['id', 'operation', 'original', 'substitution']
|
||||
search_fields = ['id', 'operation', 'original', 'substitution']
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# 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',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
# 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',
|
||||
),
|
||||
]
|
|
@ -2,15 +2,18 @@
|
|||
from django.db.models import (
|
||||
CASCADE,
|
||||
SET_NULL,
|
||||
BooleanField,
|
||||
CharField,
|
||||
FloatField,
|
||||
ForeignKey,
|
||||
Model,
|
||||
QuerySet,
|
||||
TextChoices,
|
||||
TextField
|
||||
)
|
||||
|
||||
from .Argument import Argument
|
||||
from .Substitution import Substitution
|
||||
|
||||
|
||||
class OperationType(TextChoices):
|
||||
''' Type of operation. '''
|
||||
|
@ -39,10 +42,6 @@ class Operation(Model):
|
|||
on_delete=SET_NULL,
|
||||
related_name='producer'
|
||||
)
|
||||
sync_text: BooleanField = BooleanField(
|
||||
verbose_name='Синхронизация',
|
||||
default=True
|
||||
)
|
||||
|
||||
alias: CharField = CharField(
|
||||
verbose_name='Шифр',
|
||||
|
@ -74,3 +73,11 @@ class Operation(Model):
|
|||
|
||||
def __str__(self) -> str:
|
||||
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)
|
||||
|
|
|
@ -5,10 +5,12 @@ from django.core.exceptions import ValidationError
|
|||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from apps.library.models import Editor, LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import RSForm
|
||||
from shared import messages as msg
|
||||
|
||||
from .Argument import Argument
|
||||
from .Inheritance import Inheritance
|
||||
from .Operation import Operation
|
||||
from .Substitution import Substitution
|
||||
|
||||
|
@ -76,8 +78,8 @@ class OperationSchema:
|
|||
''' Delete operation. '''
|
||||
operation.delete()
|
||||
|
||||
# deal with attached schema
|
||||
# trigger on_change effects
|
||||
# TODO: deal with attached schema
|
||||
# TODO: trigger on_change effects
|
||||
|
||||
self.save()
|
||||
|
||||
|
@ -86,53 +88,124 @@ class OperationSchema:
|
|||
''' Set input schema for operation. '''
|
||||
if schema == target.result:
|
||||
return
|
||||
if schema:
|
||||
target.result = schema
|
||||
if schema is not None:
|
||||
target.result = schema
|
||||
target.alias = schema.alias
|
||||
target.title = schema.title
|
||||
target.comment = schema.comment
|
||||
else:
|
||||
target.result = None
|
||||
target.save()
|
||||
|
||||
# trigger on_change effects
|
||||
# TODO: trigger on_change effects
|
||||
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]:
|
||||
''' Add Argument to operation. '''
|
||||
if Argument.objects.filter(operation=operation, argument=argument).exists():
|
||||
return None
|
||||
result = Argument.objects.create(operation=operation, argument=argument)
|
||||
self.save()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
def clear_arguments(self, target: Operation):
|
||||
''' Clear all arguments for operation. '''
|
||||
if not Argument.objects.filter(operation=target).exists():
|
||||
def set_arguments(self, operation: Operation, arguments: list[Operation]):
|
||||
''' Set arguments to operation. '''
|
||||
processed: list[Operation] = []
|
||||
changed = False
|
||||
for current in operation.getArguments():
|
||||
if current.argument not in arguments:
|
||||
changed = True
|
||||
current.delete()
|
||||
else:
|
||||
processed.append(current.argument)
|
||||
for arg in arguments:
|
||||
if arg not in processed:
|
||||
changed = True
|
||||
processed.append(arg)
|
||||
Argument.objects.create(operation=operation, argument=arg)
|
||||
if not changed:
|
||||
return
|
||||
|
||||
Argument.objects.filter(operation=target).delete()
|
||||
Substitution.objects.filter(operation=target).delete()
|
||||
|
||||
# trigger on_change effects
|
||||
|
||||
# TODO: trigger on_change effects
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_substitutions(self, target: Operation, substitutes: list[dict]):
|
||||
''' Clear all arguments for operation. '''
|
||||
Substitution.objects.filter(operation=target).delete()
|
||||
for sub in substitutes:
|
||||
Substitution.objects.create(
|
||||
operation=target,
|
||||
original=sub['original'],
|
||||
substitution=sub['substitution'],
|
||||
transfer_term=sub['transfer_term']
|
||||
)
|
||||
processed: list[dict] = []
|
||||
changed = False
|
||||
|
||||
# trigger on_change effects
|
||||
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:
|
||||
if sub not in processed:
|
||||
changed = True
|
||||
Substitution.objects.create(
|
||||
operation=target,
|
||||
original=sub['original'],
|
||||
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()
|
||||
receiver.reset_aliases()
|
||||
self.save()
|
||||
return True
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
''' Models: Synthesis Substitution. '''
|
||||
from django.db.models import CASCADE, BooleanField, ForeignKey, Model
|
||||
from django.db.models import CASCADE, ForeignKey, Model
|
||||
|
||||
|
||||
class Substitution(Model):
|
||||
|
@ -22,10 +22,6 @@ class Substitution(Model):
|
|||
on_delete=CASCADE,
|
||||
related_name='as_substitute'
|
||||
)
|
||||
transfer_term: BooleanField = BooleanField(
|
||||
verbose_name='Перенос термина',
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
|
|
|
@ -6,6 +6,8 @@ from .data_access import (
|
|||
OperationCreateSerializer,
|
||||
OperationSchemaSerializer,
|
||||
OperationSerializer,
|
||||
OperationTargetSerializer
|
||||
OperationTargetSerializer,
|
||||
OperationUpdateSerializer,
|
||||
SetOperationInputSerializer
|
||||
)
|
||||
from .responses import NewOperationResponse, NewSchemaResponse
|
||||
|
|
|
@ -21,7 +21,6 @@ class SubstitutionExSerializer(serializers.Serializer):
|
|||
operation = serializers.IntegerField()
|
||||
original = serializers.IntegerField()
|
||||
substitution = serializers.IntegerField()
|
||||
transfer_term = serializers.BooleanField()
|
||||
original_alias = serializers.CharField()
|
||||
original_term = serializers.CharField()
|
||||
substitution_alias = serializers.CharField()
|
||||
|
|
|
@ -5,8 +5,10 @@ from django.db.models import F
|
|||
from rest_framework import serializers
|
||||
from rest_framework.serializers import PrimaryKeyRelatedField as PKField
|
||||
|
||||
from apps.library.models import LibraryItem
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
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 ..models import Argument, Operation, OperationSchema, OperationType
|
||||
|
@ -32,7 +34,7 @@ class ArgumentSerializer(serializers.ModelSerializer):
|
|||
|
||||
class OperationCreateSerializer(serializers.Serializer):
|
||||
''' Serializer: Operation creation. '''
|
||||
class OperationData(serializers.ModelSerializer):
|
||||
class OperationCreateData(serializers.ModelSerializer):
|
||||
''' Serializer: Operation creation data. '''
|
||||
alias = serializers.CharField()
|
||||
operation_type = serializers.ChoiceField(OperationType.choices)
|
||||
|
@ -41,18 +43,83 @@ class OperationCreateSerializer(serializers.Serializer):
|
|||
''' serializer metadata. '''
|
||||
model = Operation
|
||||
fields = \
|
||||
'alias', 'operation_type', 'title', 'sync_text', \
|
||||
'alias', 'operation_type', 'title', \
|
||||
'comment', 'result', 'position_x', 'position_y'
|
||||
|
||||
create_schema = serializers.BooleanField(default=False, required=False)
|
||||
item_data = OperationData()
|
||||
item_data = OperationCreateData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
|
||||
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
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):
|
||||
''' Serializer: Delete operation. '''
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
|
@ -66,9 +133,36 @@ class OperationTargetSerializer(serializers.Serializer):
|
|||
operation = cast(Operation, attrs['target'])
|
||||
if oss and operation.oss != oss:
|
||||
raise serializers.ValidationError({
|
||||
f'{operation.id}': msg.operationNotOwned(oss.title)
|
||||
'target': msg.operationNotInOSS(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
|
||||
|
||||
|
||||
|
@ -103,7 +197,6 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
|||
'operation',
|
||||
'original',
|
||||
'substitution',
|
||||
'transfer_term',
|
||||
original_alias=F('original__alias'),
|
||||
original_term=F('original__term_resolved'),
|
||||
substitution_alias=F('substitution__alias'),
|
||||
|
|
|
@ -29,7 +29,6 @@ class TestOperation(TestCase):
|
|||
self.assertEqual(self.operation.alias, 'KS1')
|
||||
self.assertEqual(self.operation.title, '')
|
||||
self.assertEqual(self.operation.comment, '')
|
||||
self.assertEqual(self.operation.sync_text, True)
|
||||
self.assertEqual(self.operation.position_x, 0)
|
||||
self.assertEqual(self.operation.position_y, 0)
|
||||
|
||||
|
@ -50,15 +49,6 @@ class TestOperation(TestCase):
|
|||
self.assertEqual(self.operation.title, schema.model.title)
|
||||
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):
|
||||
schema = LibraryItem.objects.create(alias=self.operation.alias, item_type=LibraryItemType.RSFORM)
|
||||
self.operation.result = schema
|
||||
|
|
|
@ -47,8 +47,7 @@ class TestSynthesisSubstitution(TestCase):
|
|||
self.substitution = Substitution.objects.create(
|
||||
operation=self.operation3,
|
||||
original=self.ks1x1,
|
||||
substitution=self.ks2x1,
|
||||
transfer_term=False
|
||||
substitution=self.ks2x1
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from rest_framework import status
|
||||
|
||||
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType, LocationHead
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
@ -23,10 +23,27 @@ class TestOssViewset(EndpointTester):
|
|||
|
||||
|
||||
def populateData(self):
|
||||
self.ks1 = RSForm.create(alias='KS1', title='Test1', owner=self.user)
|
||||
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
||||
self.ks2 = RSForm.create(alias='KS2', title='Test2', owner=self.user)
|
||||
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||
self.ks1 = RSForm.create(
|
||||
alias='KS1',
|
||||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
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(
|
||||
alias='1',
|
||||
operation_type=OperationType.INPUT,
|
||||
|
@ -41,12 +58,10 @@ class TestOssViewset(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.add_argument(self.operation3, self.operation1)
|
||||
self.owned.add_argument(self.operation3, self.operation2)
|
||||
self.owned.set_arguments(self.operation3, [self.operation1, self.operation2])
|
||||
self.owned.set_substitutions(self.operation3, [{
|
||||
'original': self.ks1x1,
|
||||
'substitution': self.ks2x1,
|
||||
'transfer_term': False
|
||||
'substitution': self.ks2x1
|
||||
}])
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/details', method='get')
|
||||
|
@ -72,7 +87,6 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(sub['operation'], self.operation3.pk)
|
||||
self.assertEqual(sub['original'], self.ks1x1.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_term'], self.ks1x1.term_resolved)
|
||||
self.assertEqual(sub['substitution_alias'], self.ks2x1.alias)
|
||||
|
@ -135,7 +149,6 @@ class TestOssViewset(EndpointTester):
|
|||
'alias': 'Test3',
|
||||
'title': 'Test title',
|
||||
'comment': 'Тест кириллицы',
|
||||
'sync_text': False,
|
||||
'position_x': 1,
|
||||
'position_y': 1,
|
||||
},
|
||||
|
@ -160,7 +173,6 @@ class TestOssViewset(EndpointTester):
|
|||
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_y'], data['item_data']['position_y'])
|
||||
self.assertEqual(new_operation['sync_text'], data['item_data']['sync_text'])
|
||||
self.assertEqual(new_operation['result'], None)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
|
||||
|
@ -208,6 +220,7 @@ class TestOssViewset(EndpointTester):
|
|||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_schema(self):
|
||||
self.populateData()
|
||||
Editor.add(self.owned.model, self.user2)
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
|
@ -228,6 +241,7 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(schema.visible, False)
|
||||
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
||||
self.assertEqual(schema.location, self.owned.model.location)
|
||||
self.assertIn(self.user2, schema.editors())
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation(self):
|
||||
|
@ -273,13 +287,11 @@ class TestOssViewset(EndpointTester):
|
|||
self.operation1.result = None
|
||||
self.operation1.comment = 'TestComment'
|
||||
self.operation1.title = 'TestTitle'
|
||||
self.operation1.sync_text = False
|
||||
self.operation1.save()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
|
||||
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['alias'], self.operation1.alias)
|
||||
self.assertEqual(new_schema['title'], self.operation1.title)
|
||||
|
@ -287,3 +299,178 @@ class TestOssViewset(EndpointTester):
|
|||
|
||||
data['target'] = self.operation3.pk
|
||||
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)
|
||||
|
|
|
@ -35,7 +35,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'create_operation',
|
||||
'delete_operation',
|
||||
'update_positions',
|
||||
'create_input'
|
||||
'create_input',
|
||||
'set_input',
|
||||
'update_operation',
|
||||
'execute_operation'
|
||||
]:
|
||||
permission_list = [permissions.ItemEditor]
|
||||
elif self.action in ['details']:
|
||||
|
@ -100,25 +103,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
data: dict = serializer.validated_data['item_data']
|
||||
if data['operation_type'] == m.OperationType.INPUT and serializer.validated_data['create_schema']:
|
||||
schema = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
owner=oss.model.owner,
|
||||
alias=data['alias'],
|
||||
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)
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']:
|
||||
oss.create_input(new_operation)
|
||||
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()
|
||||
oss.set_arguments(
|
||||
operation=new_operation,
|
||||
arguments=serializer.validated_data['arguments']
|
||||
)
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
|
@ -152,7 +144,6 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss.update_positions(serializer.validated_data['positions'])
|
||||
oss.delete_operation(serializer.validated_data['target'])
|
||||
|
||||
oss.refresh_from_db()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
|
@ -191,25 +182,127 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss.update_positions(serializer.validated_data['positions'])
|
||||
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()
|
||||
schema = oss.create_input(operation)
|
||||
|
||||
oss.refresh_from_db()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
'new_schema': LibraryItemSerializer(schema).data,
|
||||
'new_schema': LibraryItemSerializer(schema.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
|
||||
)
|
||||
|
|
|
@ -12,7 +12,6 @@ from django.db.models import (
|
|||
TextChoices,
|
||||
TextField
|
||||
)
|
||||
from django.urls import reverse
|
||||
|
||||
from ..utils import apply_pattern
|
||||
|
||||
|
@ -95,10 +94,6 @@ class Constituenta(Model):
|
|||
verbose_name = 'Конституента'
|
||||
verbose_name_plural = 'Конституенты'
|
||||
|
||||
def get_absolute_url(self):
|
||||
''' URL access. '''
|
||||
return reverse('constituenta-detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.alias}'
|
||||
|
||||
|
|
|
@ -241,18 +241,12 @@ class RSForm:
|
|||
def substitute(
|
||||
self,
|
||||
original: Constituenta,
|
||||
substitution: Constituenta,
|
||||
transfer_term: bool
|
||||
substitution: Constituenta
|
||||
):
|
||||
''' Execute constituenta substitution. '''
|
||||
assert original.pk != substitution.pk
|
||||
mapping = {original.alias: substitution.alias}
|
||||
self.apply_mapping(mapping)
|
||||
if transfer_term:
|
||||
substitution.term_raw = original.term_raw
|
||||
substitution.term_forms = original.term_forms
|
||||
substitution.term_resolved = original.term_resolved
|
||||
substitution.save()
|
||||
original.delete()
|
||||
self.on_term_change([substitution.id])
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ from .data_access import (
|
|||
CstTargetSerializer,
|
||||
InlineSynthesisSerializer,
|
||||
RSFormParseSerializer,
|
||||
RSFormSerializer
|
||||
RSFormSerializer,
|
||||
SubstitutionSerializerBase
|
||||
)
|
||||
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
|
||||
from .io_pyconcept import PyConceptAdapter
|
||||
|
|
|
@ -31,7 +31,7 @@ class CstSerializer(serializers.ModelSerializer):
|
|||
''' serializer metadata. '''
|
||||
model = Constituenta
|
||||
fields = '__all__'
|
||||
read_only_fields = ('id', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
|
||||
read_only_fields = ('id', 'schema', 'order', 'alias', 'cst_type', 'definition_resolved', 'term_resolved')
|
||||
|
||||
def update(self, instance: Constituenta, validated_data) -> Constituenta:
|
||||
data = validated_data # Note: use alias for better code readability
|
||||
|
@ -212,7 +212,7 @@ class CstTargetSerializer(serializers.Serializer):
|
|||
cst = cast(Constituenta, attrs['target'])
|
||||
if schema and cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
f'{cst.id}': msg.constituentaNotOwned(schema.title)
|
||||
f'{cst.id}': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
if cst.cst_type not in [CstType.FUNCTION, CstType.STRUCTURED, CstType.TERM]:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -234,7 +234,7 @@ class CstRenameSerializer(serializers.Serializer):
|
|||
cst = cast(Constituenta, attrs['target'])
|
||||
if cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
f'{cst.id}': msg.constituentaNotOwned(schema.title)
|
||||
f'{cst.id}': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
new_alias = self.initial_data['alias']
|
||||
if cst.alias == new_alias:
|
||||
|
@ -260,7 +260,7 @@ class CstListSerializer(serializers.Serializer):
|
|||
for item in attrs['items']:
|
||||
if item.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
f'{item.id}': msg.constituentaNotOwned(schema.title)
|
||||
f'{item.id}': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
@ -270,17 +270,16 @@ class CstMoveSerializer(CstListSerializer):
|
|||
move_to = serializers.IntegerField()
|
||||
|
||||
|
||||
class CstSubstituteSerializerBase(serializers.Serializer):
|
||||
class SubstitutionSerializerBase(serializers.Serializer):
|
||||
''' Serializer: Basic substitution. '''
|
||||
original = 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):
|
||||
''' Serializer: Constituenta substitution. '''
|
||||
substitutions = serializers.ListField(
|
||||
child=CstSubstituteSerializerBase(),
|
||||
child=SubstitutionSerializerBase(),
|
||||
min_length=1
|
||||
)
|
||||
|
||||
|
@ -300,11 +299,11 @@ class CstSubstituteSerializer(serializers.Serializer):
|
|||
})
|
||||
if original_cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
'original': msg.constituentaNotOwned(schema.title)
|
||||
'original': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
if substitution_cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
'substitution': msg.constituentaNotOwned(schema.title)
|
||||
'substitution': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
deleted.add(original_cst.pk)
|
||||
return attrs
|
||||
|
@ -316,7 +315,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
|
|||
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
|
||||
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||
substitutions = serializers.ListField(
|
||||
child=CstSubstituteSerializerBase()
|
||||
child=SubstitutionSerializerBase()
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
|
@ -325,14 +324,14 @@ class InlineSynthesisSerializer(serializers.Serializer):
|
|||
schema_out = cast(LibraryItem, attrs['receiver'])
|
||||
if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
|
||||
raise PermissionDenied({
|
||||
'message': msg.schemaNotOwned(),
|
||||
'message': msg.schemaForbidden(),
|
||||
'object_id': schema_in.id
|
||||
})
|
||||
constituents = cast(list[Constituenta], attrs['items'])
|
||||
for cst in constituents:
|
||||
if cst.schema != schema_in:
|
||||
raise serializers.ValidationError({
|
||||
f'{cst.id}': msg.constituentaNotOwned(schema_in.title)
|
||||
f'{cst.id}': msg.constituentaNotInRSform(schema_in.title)
|
||||
})
|
||||
deleted = set()
|
||||
for item in attrs['substitutions']:
|
||||
|
@ -345,7 +344,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
|
|||
})
|
||||
if substitution_cst.schema != schema_out:
|
||||
raise serializers.ValidationError({
|
||||
f'{substitution_cst.id}': msg.constituentaNotOwned(schema_out.title)
|
||||
f'{substitution_cst.id}': msg.constituentaNotInRSform(schema_out.title)
|
||||
})
|
||||
else:
|
||||
if substitution_cst not in constituents:
|
||||
|
@ -354,7 +353,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
|
|||
})
|
||||
if original_cst.schema != schema_out:
|
||||
raise serializers.ValidationError({
|
||||
f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title)
|
||||
f'{original_cst.id}': msg.constituentaNotInRSform(schema_out.title)
|
||||
})
|
||||
if original_cst.pk in deleted:
|
||||
raise serializers.ValidationError({
|
||||
|
|
|
@ -20,12 +20,6 @@ class TestConstituenta(TestCase):
|
|||
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):
|
||||
with self.assertRaises(IntegrityError):
|
||||
Constituenta.objects.create(alias='X1', schema=self.schema1.model)
|
||||
|
|
|
@ -208,11 +208,11 @@ class TestRSForm(TestCase):
|
|||
definition_formal=x1.alias
|
||||
)
|
||||
|
||||
self.schema.substitute(x1, x2, True)
|
||||
self.schema.substitute(x1, x2)
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituents().count(), 2)
|
||||
self.assertEqual(x2.term_raw, 'Test')
|
||||
self.assertEqual(x2.term_raw, 'Test2')
|
||||
self.assertEqual(d1.definition_formal, x2.alias)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
''' Tests for REST API. '''
|
||||
from .t_cctext import *
|
||||
from .t_constituents import *
|
||||
from .t_operations import *
|
||||
from .t_rsforms import *
|
||||
from .t_rslang import *
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
''' 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)
|
|
@ -1,82 +0,0 @@
|
|||
''' 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')
|
|
@ -272,43 +272,6 @@ class TestRSFormViewset(EndpointTester):
|
|||
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')
|
||||
def test_substitute_multiple(self):
|
||||
self.set_params(item=self.owned_id)
|
||||
|
@ -327,13 +290,11 @@ class TestRSFormViewset(EndpointTester):
|
|||
data = {'substitutions': [
|
||||
{
|
||||
'original': x1.pk,
|
||||
'substitution': d1.pk,
|
||||
'transfer_term': True
|
||||
'substitution': d1.pk
|
||||
},
|
||||
{
|
||||
'original': x1.pk,
|
||||
'substitution': d2.pk,
|
||||
'transfer_term': True
|
||||
'substitution': d2.pk
|
||||
}
|
||||
]}
|
||||
self.executeBadData(data=data)
|
||||
|
@ -341,13 +302,11 @@ class TestRSFormViewset(EndpointTester):
|
|||
data = {'substitutions': [
|
||||
{
|
||||
'original': x1.pk,
|
||||
'substitution': d1.pk,
|
||||
'transfer_term': True
|
||||
'substitution': d1.pk
|
||||
},
|
||||
{
|
||||
'original': x2.pk,
|
||||
'substitution': d2.pk,
|
||||
'transfer_term': True
|
||||
'substitution': d2.pk
|
||||
}
|
||||
]}
|
||||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
|
@ -523,3 +482,172 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.assertEqual(len(items), 2)
|
||||
self.assertEqual(items[0]['order'], f1.order + 1)
|
||||
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')
|
||||
|
|
|
@ -9,11 +9,9 @@ library_router.register('rsforms', views.RSFormViewSet, 'RSForm')
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
path('constituents/<int:pk>', views.ConstituentAPIView.as_view(), name='constituenta-detail'),
|
||||
path('rsforms/import-trs', views.TrsImportView.as_view()),
|
||||
path('rsforms/create-detailed', views.create_rsform),
|
||||
|
||||
path('operations/inline-synthesis', views.inline_synthesis),
|
||||
path('rsforms/inline-synthesis', views.inline_synthesis),
|
||||
|
||||
path('rslang/parse-expression', views.parse_expression),
|
||||
path('rslang/to-ascii', views.convert_to_ascii),
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
''' REST API: Endpoint processors. '''
|
||||
from .cctext import generate_lexeme, inflect, parse_text
|
||||
from .constituents import ConstituentAPIView
|
||||
from .operations import inline_synthesis
|
||||
from .rsforms import RSFormViewSet, TrsImportView, create_rsform
|
||||
from .rsforms import RSFormViewSet, TrsImportView, create_rsform, inline_synthesis
|
||||
from .rslang import convert_to_ascii, convert_to_math, parse_expression
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
''' 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
|
|
@ -1,50 +0,0 @@
|
|||
''' 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
|
||||
)
|
|
@ -12,6 +12,7 @@ from rest_framework import views, viewsets
|
|||
from rest_framework.decorators import action, api_view
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||
from apps.library.serializers import LibraryItemSerializer
|
||||
|
@ -45,7 +46,8 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
'substitute',
|
||||
'restore_order',
|
||||
'reset_aliases',
|
||||
'produce_structure'
|
||||
'produce_structure',
|
||||
'update_cst'
|
||||
]:
|
||||
permission_list = [permissions.ItemEditor]
|
||||
elif self.action in [
|
||||
|
@ -88,15 +90,43 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
new_cst = m.RSForm(schema).create_cst(data, insert_after)
|
||||
|
||||
schema.refresh_from_db()
|
||||
response = Response(
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_cst': s.CstSerializer(new_cst).data,
|
||||
'schema': s.RSFormParseSerializer(schema).data
|
||||
}
|
||||
)
|
||||
response['Location'] = new_cst.get_absolute_url()
|
||||
return response
|
||||
|
||||
@extend_schema(
|
||||
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(
|
||||
summary='produce the structure of a given constituenta',
|
||||
|
@ -174,7 +204,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='substitute constituenta',
|
||||
summary='execute substitutions',
|
||||
tags=['RSForm'],
|
||||
request=s.CstSubstituteSerializer,
|
||||
responses={
|
||||
|
@ -198,7 +228,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
for substitution in serializer.validated_data['substitutions']:
|
||||
original = cast(m.Constituenta, substitution['original'])
|
||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||
m.RSForm(schema).substitute(original, replacement, substitution['transfer_term'])
|
||||
m.RSForm(schema).substitute(original, replacement)
|
||||
|
||||
schema.refresh_from_db()
|
||||
return Response(
|
||||
|
@ -521,3 +551,41 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
|
|||
|
||||
data['access_policy'] = request.data.get('access_policy', AccessPolicy.PUBLIC)
|
||||
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
|
||||
)
|
||||
|
|
|
@ -2,19 +2,27 @@
|
|||
# pylint: skip-file
|
||||
|
||||
|
||||
def constituentaNotOwned(title: str):
|
||||
def constituentaNotInRSform(title: str):
|
||||
return f'Конституента не принадлежит схеме: {title}'
|
||||
|
||||
|
||||
def operationNotOwned(title: str):
|
||||
return f'Операция не принадлежит схеме: {title}'
|
||||
def constituentaNotFromOperation():
|
||||
return f'Конституента не соответствую аргументам операции'
|
||||
|
||||
|
||||
def operationNotInOSS(title: str):
|
||||
return f'Операция не принадлежит ОСС: {title}'
|
||||
|
||||
|
||||
def previousResultMissing():
|
||||
return 'Отсутствует результат предыдущей операции'
|
||||
|
||||
|
||||
def substitutionNotInList():
|
||||
return 'Отождествляемая конституента отсутствует в списке'
|
||||
|
||||
|
||||
def schemaNotOwned():
|
||||
def schemaForbidden():
|
||||
return 'Нет доступа к схеме'
|
||||
|
||||
|
||||
|
@ -22,6 +30,10 @@ def operationNotInput(title: str):
|
|||
return f'Операция не является Загрузкой: {title}'
|
||||
|
||||
|
||||
def operationNotSynthesis(title: str):
|
||||
return f'Операция не является Синтезом: {title}'
|
||||
|
||||
|
||||
def operationResultNotEmpty(title: str):
|
||||
return f'Результат операции не пуст: {title}'
|
||||
|
||||
|
|
|
@ -32,6 +32,24 @@ 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):
|
||||
''' Item permission: Admin or higher. '''
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
});
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* 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
|
||||
});
|
||||
}
|
|
@ -7,6 +7,8 @@ import {
|
|||
IOperationCreateData,
|
||||
IOperationCreatedResponse,
|
||||
IOperationSchemaData,
|
||||
IOperationSetInputData,
|
||||
IOperationUpdateData,
|
||||
IPositionsData,
|
||||
ITargetOperation
|
||||
} from '@/models/oss';
|
||||
|
@ -50,3 +52,24 @@ export function patchCreateInput(oss: string, request: FrontExchange<ITargetOper
|
|||
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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,10 +6,13 @@ import { ILibraryCreateData, ILibraryItem } from '@/models/library';
|
|||
import { ICstSubstituteData } from '@/models/oss';
|
||||
import {
|
||||
IConstituentaList,
|
||||
IConstituentaMeta,
|
||||
ICstCreateData,
|
||||
ICstCreatedResponse,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstUpdateData,
|
||||
IInlineSynthesisData,
|
||||
IProduceStructureResponse,
|
||||
IRSFormData,
|
||||
IRSFormUploadData,
|
||||
|
@ -68,6 +71,13 @@ 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>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/rsforms/${schema}/delete-multiple-cst`,
|
||||
|
@ -135,3 +145,10 @@ export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUpl
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/rsforms/inline-synthesis`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
|
|
@ -89,8 +89,6 @@ export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
|
|||
export { BiPauseCircle as IconStatusIncalculable } from 'react-icons/bi';
|
||||
export { LuPower as IconKeepAliasOn } 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 =====
|
||||
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
|
||||
|
@ -108,7 +106,7 @@ export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
|
|||
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
|
||||
export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
|
||||
export { GrConnect as IconConnect } from 'react-icons/gr';
|
||||
export { BsPlay as IconExecute } from 'react-icons/bs';
|
||||
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
||||
|
||||
// ======== Graph UI =======
|
||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||
|
|
|
@ -1,105 +1,126 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import BadgeConstituenta from '@/components/info/BadgeConstituenta';
|
||||
import SelectConstituenta from '@/components/select/SelectConstituenta';
|
||||
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { IConstituenta, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
import { describeConstituenta } from '@/utils/labels';
|
||||
import { ILibraryItem } from '@/models/library';
|
||||
import { ICstSubstitute, IMultiSubstitution } from '@/models/oss';
|
||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { errors } from '@/utils/labels';
|
||||
|
||||
import {
|
||||
IconKeepAliasOff,
|
||||
IconKeepAliasOn,
|
||||
IconKeepTermOff,
|
||||
IconKeepTermOn,
|
||||
IconPageFirst,
|
||||
IconPageLast,
|
||||
IconPageLeft,
|
||||
IconPageRight,
|
||||
IconRemove,
|
||||
IconReplace
|
||||
} from '../Icons';
|
||||
import { IconPageLeft, IconPageRight, IconRemove, IconReplace } from '../Icons';
|
||||
import NoData from '../ui/NoData';
|
||||
import SelectLibraryItem from './SelectLibraryItem';
|
||||
|
||||
interface PickSubstitutionsProps {
|
||||
substitutions: ICstSubstitute[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
||||
|
||||
prefixID: string;
|
||||
rows?: number;
|
||||
allowSelfSubstitution?: boolean;
|
||||
|
||||
schema1?: IRSForm;
|
||||
schema2?: IRSForm;
|
||||
filter1?: (cst: IConstituenta) => boolean;
|
||||
filter2?: (cst: IConstituenta) => boolean;
|
||||
|
||||
items: ISingleSubstitution[];
|
||||
setItems: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
|
||||
schemas: IRSForm[];
|
||||
filter?: (cst: IConstituenta) => boolean;
|
||||
}
|
||||
|
||||
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>();
|
||||
const columnHelper = createColumnHelper<IMultiSubstitution>();
|
||||
|
||||
function PickSubstitutions({
|
||||
items,
|
||||
schema1,
|
||||
schema2,
|
||||
filter1,
|
||||
filter2,
|
||||
substitutions,
|
||||
setSubstitutions,
|
||||
prefixID,
|
||||
rows,
|
||||
setItems,
|
||||
prefixID
|
||||
schemas,
|
||||
filter,
|
||||
allowSelfSubstitution
|
||||
}: PickSubstitutionsProps) {
|
||||
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 [rightCst, setRightCst] = useState<IConstituenta | undefined>(undefined);
|
||||
const [deleteRight, setDeleteRight] = useState(true);
|
||||
const [takeLeftTerm, setTakeLeftTerm] = useState(true);
|
||||
|
||||
const [deleteRight, setDeleteRight] = useState(true);
|
||||
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() {
|
||||
if (!leftCst || !rightCst) {
|
||||
return;
|
||||
}
|
||||
const newSubstitution: ISingleSubstitution = {
|
||||
leftCst: leftCst,
|
||||
rightCst: rightCst,
|
||||
deleteRight: deleteRight,
|
||||
takeLeftTerm: takeLeftTerm
|
||||
const newSubstitution: ICstSubstitute = {
|
||||
original: deleteRight ? rightCst.id : leftCst.id,
|
||||
substitution: deleteRight ? leftCst.id : rightCst.id
|
||||
};
|
||||
setItems([
|
||||
newSubstitution,
|
||||
...items.filter(
|
||||
item =>
|
||||
(!item.deleteRight && item.leftCst.id !== leftCst.id) ||
|
||||
(item.deleteRight && item.rightCst.id !== rightCst.id)
|
||||
)
|
||||
]);
|
||||
const toDelete = substitutions.map(item => item.original);
|
||||
const replacements = substitutions.map(item => item.substitution);
|
||||
console.log(toDelete, replacements);
|
||||
console.log(newSubstitution);
|
||||
if (
|
||||
toDelete.includes(newSubstitution.original) ||
|
||||
toDelete.includes(newSubstitution.substitution) ||
|
||||
replacements.includes(newSubstitution.original)
|
||||
) {
|
||||
toast.error(errors.reuseOriginal);
|
||||
return;
|
||||
}
|
||||
setSubstitutions(prev => [...prev, newSubstitution]);
|
||||
setLeftCst(undefined);
|
||||
setRightCst(undefined);
|
||||
}
|
||||
|
||||
const handleDeleteRow = useCallback(
|
||||
(row: number) => {
|
||||
setItems(prev => {
|
||||
const newItems: ISingleSubstitution[] = [];
|
||||
setSubstitutions(prev => {
|
||||
const newItems: ICstSubstitute[] = [];
|
||||
prev.forEach((item, index) => {
|
||||
if (index !== row) {
|
||||
newItems.push(item);
|
||||
|
@ -108,54 +129,62 @@ function PickSubstitutions({
|
|||
return newItems;
|
||||
});
|
||||
},
|
||||
[setItems]
|
||||
[setSubstitutions]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor(item => describeConstituenta(item.leftCst), {
|
||||
id: 'left_text',
|
||||
header: 'Описание',
|
||||
size: 1000,
|
||||
cell: props => <div className='text-xs text-ellipsis'>{props.getValue()}</div>
|
||||
columnHelper.accessor(item => item.substitution_source?.alias ?? 'N/A', {
|
||||
id: 'left_schema',
|
||||
header: 'Операция',
|
||||
size: 100,
|
||||
cell: props => <div className='min-w-[10.5rem] text-ellipsis text-right'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.accessor(item => item.leftCst.alias, {
|
||||
columnHelper.accessor(item => item.substitution?.alias ?? 'N/A', {
|
||||
id: 'left_alias',
|
||||
header: () => <span className='pl-3'>Имя</span>,
|
||||
size: 65,
|
||||
cell: props => (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.leftCst} prefixID={`${prefixID}_1_`} />
|
||||
)
|
||||
cell: props =>
|
||||
props.row.original.substitution ? (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.substitution} prefixID={`${prefixID}_1_`} />
|
||||
) : (
|
||||
'N/A'
|
||||
)
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
header: '',
|
||||
size: 40,
|
||||
cell: props => <SubstitutionIcon item={props.row.original} />
|
||||
cell: () => <IconPageRight size='1.2rem' />
|
||||
}),
|
||||
columnHelper.accessor(item => item.rightCst.alias, {
|
||||
columnHelper.accessor(item => item.original?.alias ?? 'N/A', {
|
||||
id: 'right_alias',
|
||||
header: () => <span className='pl-3'>Имя</span>,
|
||||
size: 65,
|
||||
cell: props => (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.rightCst} prefixID={`${prefixID}_2_`} />
|
||||
)
|
||||
cell: props =>
|
||||
props.row.original.original ? (
|
||||
<BadgeConstituenta theme={colors} value={props.row.original.original} prefixID={`${prefixID}_1_`} />
|
||||
) : (
|
||||
'N/A'
|
||||
)
|
||||
}),
|
||||
columnHelper.accessor(item => describeConstituenta(item.rightCst), {
|
||||
id: 'right_text',
|
||||
header: 'Описание',
|
||||
minSize: 1000,
|
||||
cell: props => <div className='text-xs text-ellipsis text-pretty'>{props.getValue()}</div>
|
||||
columnHelper.accessor(item => item.original_source?.alias ?? 'N/A', {
|
||||
id: 'right_schema',
|
||||
header: 'Операция',
|
||||
size: 100,
|
||||
cell: props => <div className='min-w-[8rem] text-ellipsis'>{props.getValue()}</div>
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
cell: props => (
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Удалить'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDeleteRow(props.row.index)}
|
||||
/>
|
||||
<div className='max-w-fit'>
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Удалить'
|
||||
icon={<IconRemove size='1rem' className='icon-red' />}
|
||||
onClick={() => handleDeleteRow(props.row.index)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
],
|
||||
|
@ -165,87 +194,65 @@ function PickSubstitutions({
|
|||
return (
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='flex items-end gap-3 justify-stretch'>
|
||||
<div className='flex-grow basis-1/2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label text={schema1 !== schema2 ? schema1?.alias ?? 'Схема 1' : ''} />
|
||||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
title='Сохранить конституенту'
|
||||
noHover
|
||||
onClick={toggleDelete}
|
||||
icon={
|
||||
deleteRight ? (
|
||||
<IconKeepAliasOn size='1rem' className='clr-text-green' />
|
||||
) : (
|
||||
<IconKeepAliasOff size='1rem' className='clr-text-red' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сохранить термин'
|
||||
noHover
|
||||
onClick={toggleTerm}
|
||||
icon={
|
||||
takeLeftTerm ? (
|
||||
<IconKeepTermOn size='1rem' className='clr-text-green' />
|
||||
) : (
|
||||
<IconKeepTermOff size='1rem' className='clr-text-red' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-grow flex flex-col basis-1/2'>
|
||||
<div className='flex flex-col gap-[0.125rem] border-x border-t clr-input'>
|
||||
<SelectLibraryItem
|
||||
noBorder
|
||||
placeholder='Выберите аргумент'
|
||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== rightArgument?.id)}
|
||||
value={leftArgument}
|
||||
onSelectValue={setLeftArgument}
|
||||
/>
|
||||
<SelectConstituenta
|
||||
noBorder
|
||||
items={(leftArgument as IRSForm)?.items.filter(
|
||||
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
|
||||
)}
|
||||
value={leftCst}
|
||||
onSelectValue={setLeftCst}
|
||||
/>
|
||||
</div>
|
||||
<SelectConstituenta
|
||||
items={schema1?.items.filter(cst => !filter1 || filter1(cst))}
|
||||
value={leftCst}
|
||||
onSelectValue={setLeftCst}
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<MiniButton
|
||||
title={deleteRight ? 'Заменить правую' : 'Заменить левую'}
|
||||
onClick={toggleDelete}
|
||||
icon={
|
||||
deleteRight ? (
|
||||
<IconPageRight size='1.5rem' className='clr-text-primary' />
|
||||
) : (
|
||||
<IconPageLeft size='1.5rem' className='clr-text-primary' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<MiniButton
|
||||
title='Добавить в таблицу отождествлений'
|
||||
className='mb-[0.375rem] grow-0'
|
||||
icon={<IconReplace size='1.5rem' className='icon-primary' />}
|
||||
disabled={!leftCst || !rightCst || leftCst === rightCst}
|
||||
onClick={addSubstitution}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MiniButton
|
||||
noHover
|
||||
title='Добавить в таблицу отождествлений'
|
||||
className='mb-[0.375rem] grow-0'
|
||||
icon={<IconReplace size='1.5rem' className='icon-primary' />}
|
||||
disabled={!leftCst || !rightCst || leftCst === rightCst}
|
||||
onClick={addSubstitution}
|
||||
/>
|
||||
|
||||
<div className='flex-grow basis-1/2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label text={schema1 !== schema2 ? schema2?.alias ?? 'Схема 2' : ''} />
|
||||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
title='Сохранить конституенту'
|
||||
noHover
|
||||
onClick={toggleDelete}
|
||||
icon={
|
||||
!deleteRight ? (
|
||||
<IconKeepAliasOn size='1rem' className='clr-text-green' />
|
||||
) : (
|
||||
<IconKeepAliasOff size='1rem' className='clr-text-red' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сохранить термин'
|
||||
noHover
|
||||
onClick={toggleTerm}
|
||||
icon={
|
||||
!takeLeftTerm ? (
|
||||
<IconKeepTermOn size='1rem' className='clr-text-green' />
|
||||
) : (
|
||||
<IconKeepTermOff size='1rem' className='clr-text-red' />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-[0.125rem] border-x border-t clr-input'>
|
||||
<SelectLibraryItem
|
||||
noBorder
|
||||
placeholder='Выберите аргумент'
|
||||
items={allowSelfSubstitution ? schemas : schemas.filter(item => item.id !== leftArgument?.id)}
|
||||
value={rightArgument}
|
||||
onSelectValue={setRightArgument}
|
||||
/>
|
||||
<SelectConstituenta
|
||||
noBorder
|
||||
items={(rightArgument as IRSForm)?.items.filter(
|
||||
cst => !substitutions.find(item => item.original === cst.id) && (!filter || filter(cst))
|
||||
)}
|
||||
value={rightCst}
|
||||
onSelectValue={setRightCst}
|
||||
/>
|
||||
</div>
|
||||
<SelectConstituenta
|
||||
items={schema2?.items.filter(cst => !filter2 || filter2(cst))}
|
||||
value={rightCst}
|
||||
onSelectValue={setRightCst}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -256,7 +263,7 @@ function PickSubstitutions({
|
|||
className='w-full text-sm border select-none cc-scroll-y'
|
||||
rows={rows}
|
||||
contentHeight='1.3rem'
|
||||
data={items}
|
||||
data={substitutionData}
|
||||
columns={columns}
|
||||
headPosition='0'
|
||||
noDataComponent={
|
||||
|
|
|
@ -49,7 +49,7 @@ function SelectConstituenta({
|
|||
<SelectSingle
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : undefined}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${describeConstituentaTerm(value)}` } : 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}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
'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;
|
|
@ -47,7 +47,7 @@ function SelectOperation({
|
|||
<SelectSingle
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : undefined}
|
||||
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}
|
||||
|
|
|
@ -49,7 +49,7 @@ function SelectUser({
|
|||
<SelectSingle
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={value ? { value: value, label: getUserLabel(value) } : undefined}
|
||||
value={value ? { value: value, label: getUserLabel(value) } : null}
|
||||
onChange={data => {
|
||||
if (data !== null && data.value !== undefined) onSelectValue(data.value);
|
||||
}}
|
||||
|
|
|
@ -12,7 +12,15 @@ import {
|
|||
patchSetOwner,
|
||||
postSubscribe
|
||||
} from '@/backend/library';
|
||||
import { patchCreateInput, patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss';
|
||||
import {
|
||||
patchCreateInput,
|
||||
patchDeleteOperation,
|
||||
patchSetInput,
|
||||
patchUpdateOperation,
|
||||
patchUpdatePositions,
|
||||
postCreateOperation,
|
||||
postExecuteOperation
|
||||
} from '@/backend/oss';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
||||
import { ILibraryUpdateData } from '@/models/library';
|
||||
|
@ -21,6 +29,8 @@ import {
|
|||
IOperationCreateData,
|
||||
IOperationSchema,
|
||||
IOperationSchemaData,
|
||||
IOperationSetInputData,
|
||||
IOperationUpdateData,
|
||||
IPositionsData,
|
||||
ITargetOperation
|
||||
} from '@/models/oss';
|
||||
|
@ -55,6 +65,9 @@ interface IOssContext {
|
|||
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
|
||||
deleteOperation: (data: ITargetOperation, callback?: () => void) => 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);
|
||||
|
@ -333,6 +346,71 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
[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 (
|
||||
<OssContext.Provider
|
||||
value={{
|
||||
|
@ -356,7 +434,10 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
savePositions,
|
||||
createOperation,
|
||||
deleteOperation,
|
||||
createInput
|
||||
createInput,
|
||||
setInput,
|
||||
updateOperation,
|
||||
executeOperation
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { DataCallback } from '@/backend/apiTransport';
|
||||
import { patchConstituenta } from '@/backend/constituents';
|
||||
import {
|
||||
deleteUnsubscribe,
|
||||
patchLibraryItem,
|
||||
|
@ -14,16 +13,17 @@ import {
|
|||
postCreateVersion,
|
||||
postSubscribe
|
||||
} from '@/backend/library';
|
||||
import { patchInlineSynthesis } from '@/backend/operations';
|
||||
import {
|
||||
getTRSFile,
|
||||
patchDeleteConstituenta,
|
||||
patchInlineSynthesis,
|
||||
patchMoveConstituenta,
|
||||
patchProduceStructure,
|
||||
patchRenameConstituenta,
|
||||
patchResetAliases,
|
||||
patchRestoreOrder,
|
||||
patchSubstituteConstituents,
|
||||
patchUpdateConstituenta,
|
||||
patchUploadTRS,
|
||||
postCreateConstituenta
|
||||
} from '@/backend/rsforms';
|
||||
|
@ -157,7 +157,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
|
|||
onSuccess: newData => {
|
||||
setSchema(Object.assign(schema, newData));
|
||||
library.localUpdateItem(newData);
|
||||
if (callback) callback(newData);
|
||||
if (library.globalOSS?.schemas.includes(newData.id)) {
|
||||
library.reloadOSS(() => {
|
||||
if (callback) callback(newData);
|
||||
});
|
||||
} else if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -435,7 +439,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
|
|||
const cstUpdate = useCallback(
|
||||
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||
setProcessingError(undefined);
|
||||
patchConstituenta(String(data.id), {
|
||||
patchUpdateConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
|
71
rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx
Normal file
71
rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
'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;
|
|
@ -10,8 +10,8 @@ import Overlay from '@/components/ui/Overlay';
|
|||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { HelpTopic, Position2D } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { describeOperationType, labelOperationType } from '@/utils/labels';
|
||||
|
||||
|
@ -21,8 +21,6 @@ import TabSynthesisOperation from './TabSynthesisOperation';
|
|||
interface DlgCreateOperationProps {
|
||||
hideWindow: () => void;
|
||||
oss: IOperationSchema;
|
||||
positions: IOperationPosition[];
|
||||
insertPosition: Position2D;
|
||||
onCreate: (data: IOperationCreateData) => void;
|
||||
}
|
||||
|
||||
|
@ -31,7 +29,7 @@ export enum TabID {
|
|||
SYNTHESIS = 1
|
||||
}
|
||||
|
||||
function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCreate }: DlgCreateOperationProps) {
|
||||
function DlgCreateOperation({ hideWindow, oss, onCreate }: DlgCreateOperationProps) {
|
||||
const library = useLibrary();
|
||||
const [activeTab, setActiveTab] = useState(TabID.INPUT);
|
||||
|
||||
|
@ -40,7 +38,6 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
|
|||
const [comment, setComment] = useState('');
|
||||
const [inputs, setInputs] = useState<OperationID[]>([]);
|
||||
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
|
||||
const [syncText, setSyncText] = useState(true);
|
||||
const [createSchema, setCreateSchema] = useState(false);
|
||||
|
||||
const isValid = useMemo(
|
||||
|
@ -62,16 +59,15 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
|
|||
const handleSubmit = () => {
|
||||
const data: IOperationCreateData = {
|
||||
item_data: {
|
||||
position_x: insertPosition.x,
|
||||
position_y: insertPosition.y,
|
||||
position_x: 0,
|
||||
position_y: 0,
|
||||
alias: alias,
|
||||
title: title,
|
||||
comment: comment,
|
||||
sync_text: activeTab === TabID.INPUT ? syncText : true,
|
||||
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
|
||||
result: activeTab === TabID.INPUT ? attachedID ?? null : null
|
||||
},
|
||||
positions: positions,
|
||||
positions: [],
|
||||
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined,
|
||||
create_schema: createSchema
|
||||
};
|
||||
|
@ -91,14 +87,12 @@ function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCrea
|
|||
setTitle={setTitle}
|
||||
attachedID={attachedID}
|
||||
setAttachedID={setAttachedID}
|
||||
syncText={syncText}
|
||||
setSyncText={setSyncText}
|
||||
createSchema={createSchema}
|
||||
setCreateSchema={setCreateSchema}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[alias, comment, title, attachedID, syncText, oss, createSchema]
|
||||
[alias, comment, title, attachedID, oss, createSchema]
|
||||
);
|
||||
|
||||
const synthesisPanel = useMemo(
|
||||
|
|
|
@ -5,7 +5,6 @@ import { useCallback, useEffect } from 'react';
|
|||
import { IconReset } from '@/components/Icons';
|
||||
import PickSchema from '@/components/select/PickSchema';
|
||||
import Checkbox from '@/components/ui/Checkbox';
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
|
@ -25,8 +24,6 @@ interface TabInputOperationProps {
|
|||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
attachedID: LibraryItemID | undefined;
|
||||
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
|
||||
syncText: boolean;
|
||||
setSyncText: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
createSchema: boolean;
|
||||
setCreateSchema: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
@ -41,8 +38,6 @@ function TabInputOperation({
|
|||
setComment,
|
||||
attachedID,
|
||||
setAttachedID,
|
||||
syncText,
|
||||
setSyncText,
|
||||
createSchema,
|
||||
setCreateSchema
|
||||
}: TabInputOperationProps) {
|
||||
|
@ -51,9 +46,8 @@ function TabInputOperation({
|
|||
useEffect(() => {
|
||||
if (createSchema) {
|
||||
setAttachedID(undefined);
|
||||
setSyncText(true);
|
||||
}
|
||||
}, [createSchema, setAttachedID, setSyncText]);
|
||||
}, [createSchema, setAttachedID]);
|
||||
|
||||
return (
|
||||
<AnimateFade className='cc-column'>
|
||||
|
@ -62,27 +56,19 @@ function TabInputOperation({
|
|||
label='Полное название'
|
||||
value={title}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
disabled={syncText && attachedID !== undefined}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
<div className='flex gap-6'>
|
||||
<FlexColumn>
|
||||
<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)}
|
||||
disabled={syncText && attachedID !== undefined}
|
||||
/>
|
||||
<Checkbox
|
||||
value={syncText}
|
||||
setValue={setSyncText}
|
||||
label='Синхронизировать текст'
|
||||
title='Брать текст из концептуальной схемы'
|
||||
/>
|
||||
</FlexColumn>
|
||||
<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)}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
id='operation_comment'
|
||||
|
@ -91,7 +77,7 @@ function TabInputOperation({
|
|||
rows={3}
|
||||
value={comment}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
disabled={syncText && attachedID !== undefined}
|
||||
disabled={attachedID !== undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import { prefixes } from '@/utils/constants';
|
|||
import ListConstituents from './ListConstituents';
|
||||
|
||||
interface DlgDeleteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
schema: IRSForm;
|
||||
selected: ConstituentaID[];
|
||||
onDelete: (items: ConstituentaID[]) => void;
|
||||
schema: IRSForm;
|
||||
}
|
||||
|
||||
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
'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;
|
|
@ -0,0 +1,35 @@
|
|||
'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;
|
|
@ -0,0 +1,48 @@
|
|||
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;
|
|
@ -0,0 +1,31 @@
|
|||
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;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DlgEditOperation';
|
|
@ -8,7 +8,8 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
|
|||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
import { ICstSubstitute } from '@/models/oss';
|
||||
import { IInlineSynthesisData, IRSForm } from '@/models/rsform';
|
||||
|
||||
import TabConstituents from './TabConstituents';
|
||||
import TabSchema from './TabSchema';
|
||||
|
@ -30,7 +31,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
|
||||
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
|
||||
const [selected, setSelected] = useState<LibraryItemID[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
|
||||
|
||||
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });
|
||||
|
||||
|
@ -44,11 +45,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
source: source.schema?.id,
|
||||
receiver: receiver.id,
|
||||
items: selected,
|
||||
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
|
||||
}))
|
||||
substitutions: substitutions
|
||||
};
|
||||
onInlineSynthesis(data);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { ConstituentaID, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import PickSubstitutions from '../../components/select/PickSubstitutions';
|
||||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { ICstSubstitute } from '@/models/oss';
|
||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface TabSubstitutionsProps {
|
||||
receiver?: IRSForm;
|
||||
|
@ -15,8 +17,8 @@ interface TabSubstitutionsProps {
|
|||
loading?: boolean;
|
||||
error?: ErrorData;
|
||||
|
||||
substitutions: ISingleSubstitution[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
|
||||
substitutions: ICstSubstitute[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ICstSubstitute[]>>;
|
||||
}
|
||||
|
||||
function TabSubstitutions({
|
||||
|
@ -30,16 +32,22 @@ function TabSubstitutions({
|
|||
substitutions,
|
||||
setSubstitutions
|
||||
}: 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 (
|
||||
<DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={loading} error={error} hasNoData={!source}>
|
||||
<PickSubstitutions
|
||||
items={substitutions}
|
||||
setItems={setSubstitutions}
|
||||
substitutions={substitutions}
|
||||
setSubstitutions={setSubstitutions}
|
||||
rows={10}
|
||||
prefixID={prefixes.cst_inline_synth_substitutes}
|
||||
schema1={receiver}
|
||||
schema2={source}
|
||||
filter2={cst => selected.includes(cst.id)}
|
||||
schemas={schemas}
|
||||
filter={filter}
|
||||
/>
|
||||
</DataLoader>
|
||||
);
|
||||
|
|
|
@ -5,29 +5,23 @@ import { useMemo, useState } from 'react';
|
|||
|
||||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { ICstSubstituteData } from '@/models/oss';
|
||||
import { ISingleSubstitution } from '@/models/rsform';
|
||||
import { ICstSubstitute, ICstSubstituteData } from '@/models/oss';
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
schema: IRSForm;
|
||||
onSubstitute: (data: ICstSubstituteData) => void;
|
||||
}
|
||||
|
||||
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
|
||||
const { schema } = useRSForm();
|
||||
|
||||
const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
|
||||
function DlgSubstituteCst({ hideWindow, onSubstitute, schema }: DlgSubstituteCstProps) {
|
||||
const [substitutions, setSubstitutions] = useState<ICstSubstitute[]>([]);
|
||||
|
||||
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);
|
||||
|
||||
function handleSubmit() {
|
||||
const data: ICstSubstituteData = {
|
||||
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
|
||||
}))
|
||||
substitutions: substitutions
|
||||
};
|
||||
onSubstitute(data);
|
||||
}
|
||||
|
@ -43,12 +37,12 @@ function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
|
|||
className={clsx('w-[40rem]', 'px-6 pb-3')}
|
||||
>
|
||||
<PickSubstitutions
|
||||
items={substitutions}
|
||||
setItems={setSubstitutions}
|
||||
allowSelfSubstitution
|
||||
substitutions={substitutions}
|
||||
setSubstitutions={setSubstitutions}
|
||||
rows={6}
|
||||
prefixID={prefixes.dlg_cst_substitutes_list}
|
||||
schema1={schema}
|
||||
schema2={schema}
|
||||
schemas={[schema]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
|
82
rsconcept/frontend/src/hooks/useRSFormCache.ts
Normal file
82
rsconcept/frontend/src/hooks/useRSFormCache.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
'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;
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import { Graph } from './Graph';
|
||||
import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library';
|
||||
import { ConstituentaID } from './rsform';
|
||||
import { ConstituentaID, IConstituenta } from './rsform';
|
||||
|
||||
/**
|
||||
* Represents {@link IOperation} identifier type.
|
||||
|
@ -30,7 +30,6 @@ export interface IOperation {
|
|||
alias: string;
|
||||
title: string;
|
||||
comment: string;
|
||||
sync_text: boolean;
|
||||
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
|
@ -63,12 +62,28 @@ export interface ITargetOperation extends IPositionsData {
|
|||
export interface IOperationCreateData extends IPositionsData {
|
||||
item_data: Pick<
|
||||
IOperation,
|
||||
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' | 'sync_text'
|
||||
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result'
|
||||
>;
|
||||
arguments: OperationID[] | undefined;
|
||||
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.
|
||||
*/
|
||||
|
@ -83,7 +98,6 @@ export interface IArgument {
|
|||
export interface ICstSubstitute {
|
||||
original: ConstituentaID;
|
||||
substitution: ConstituentaID;
|
||||
transfer_term: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,6 +107,16 @@ export interface ICstSubstituteData {
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -241,13 +241,12 @@ export interface IVersionCreatedResponse {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents single substitution for synthesis table.
|
||||
* Represents single substitution for binary synthesis table.
|
||||
*/
|
||||
export interface ISingleSubstitution {
|
||||
export interface IBinarySubstitution {
|
||||
leftCst: IConstituenta;
|
||||
rightCst: IConstituenta;
|
||||
deleteRight: boolean;
|
||||
takeLeftTerm: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,8 +4,6 @@ import {
|
|||
IconGenerateNames,
|
||||
IconGenerateStructure,
|
||||
IconInlineSynthesis,
|
||||
IconKeepAliasOn,
|
||||
IconKeepTermOn,
|
||||
IconReplace,
|
||||
IconSortList,
|
||||
IconTemplates
|
||||
|
@ -60,13 +58,7 @@ function HelpRSLangOperations() {
|
|||
</h2>
|
||||
<p>
|
||||
Формирование таблицы отождествлений и ее применение к текущей схеме. В результате будет удален ряд конституент и
|
||||
их вхождения заменены на другие. Возможна настройка какой термин использовать для оставшихся конституент
|
||||
<li>
|
||||
<IconKeepAliasOn size='1.25rem' className='inline-icon' /> выбор сохраняемой конституенты
|
||||
</li>
|
||||
<li>
|
||||
<IconKeepTermOn size='1.25rem' className='inline-icon' /> выбор сохраняемого термина
|
||||
</li>
|
||||
их вхождения заменены на другие.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
|
||||
import Dropdown from '@/components/ui/Dropdown';
|
||||
|
@ -23,12 +22,45 @@ interface NodeContextMenuProps extends ContextMenuData {
|
|||
onHide: () => void;
|
||||
onDelete: (target: OperationID) => void;
|
||||
onCreateInput: (target: OperationID) => void;
|
||||
onEditSchema: (target: OperationID) => void;
|
||||
onEditOperation: (target: OperationID) => void;
|
||||
onExecuteOperation: (target: OperationID) => void;
|
||||
}
|
||||
|
||||
function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCreateInput }: NodeContextMenuProps) {
|
||||
function NodeContextMenu({
|
||||
operation,
|
||||
cursorX,
|
||||
cursorY,
|
||||
onHide,
|
||||
onDelete,
|
||||
onCreateInput,
|
||||
onEditSchema,
|
||||
onEditOperation,
|
||||
onExecuteOperation
|
||||
}: NodeContextMenuProps) {
|
||||
const controller = useOssEdit();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
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(() => {
|
||||
setIsOpen(false);
|
||||
|
@ -44,13 +76,13 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea
|
|||
};
|
||||
|
||||
const handleEditSchema = () => {
|
||||
toast.error('Not implemented');
|
||||
handleHide();
|
||||
onEditSchema(operation.id);
|
||||
};
|
||||
|
||||
const handleEditOperation = () => {
|
||||
toast.error('Not implemented');
|
||||
handleHide();
|
||||
onEditOperation(operation.id);
|
||||
};
|
||||
|
||||
const handleDeleteOperation = () => {
|
||||
|
@ -64,8 +96,8 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea
|
|||
};
|
||||
|
||||
const handleRunSynthesis = () => {
|
||||
toast.error('Not implemented');
|
||||
handleHide();
|
||||
onExecuteOperation(operation.id);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -97,9 +129,9 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea
|
|||
onClick={handleCreateSchema}
|
||||
/>
|
||||
) : null}
|
||||
{controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? (
|
||||
{controller.isMutable && operation.operation_type === OperationType.INPUT ? (
|
||||
<DropdownButton
|
||||
text='Загрузить схему'
|
||||
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'}
|
||||
title='Выбрать схему для загрузки'
|
||||
icon={<IconConnect size='1rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing}
|
||||
|
@ -109,9 +141,13 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea
|
|||
{controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
|
||||
<DropdownButton
|
||||
text='Выполнить синтез'
|
||||
title='Выполнить операцию и получить синтезированную КС'
|
||||
title={
|
||||
readyForSynthesis
|
||||
? 'Выполнить операцию и получить синтезированную КС'
|
||||
: 'Необходимо предоставить все аргументы'
|
||||
}
|
||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
disabled={controller.isProcessing || !readyForSynthesis}
|
||||
onClick={handleRunSynthesis}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -28,9 +28,7 @@ function OperationNode(node: OssNodeInternal) {
|
|||
noHover
|
||||
title='Связанная КС'
|
||||
hideTitle={!controller.showTooltip}
|
||||
onClick={() => {
|
||||
handleOpenSchema();
|
||||
}}
|
||||
onClick={handleOpenSchema}
|
||||
disabled={!hasFile}
|
||||
/>
|
||||
</Overlay>
|
||||
|
|
|
@ -148,6 +148,34 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
[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(() => {
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, [flow]);
|
||||
|
@ -213,6 +241,17 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
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>) {
|
||||
if (controller.isProcessing) {
|
||||
return;
|
||||
|
@ -226,6 +265,12 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
handleSavePositions();
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'q') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleCreateOperation();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -252,8 +297,9 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
onNodeClick={handleNodeClick}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
fitView
|
||||
nodeTypes={OssNodeTypes}
|
||||
maxZoom={2}
|
||||
minZoom={0.75}
|
||||
|
@ -266,7 +312,17 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
{showGrid ? <Background gap={10} /> : null}
|
||||
</ReactFlow>
|
||||
),
|
||||
[nodes, edges, handleNodesChange, handleContextMenu, handleClickCanvas, onEdgesChange, OssNodeTypes, showGrid]
|
||||
[
|
||||
nodes,
|
||||
edges,
|
||||
handleNodesChange,
|
||||
handleContextMenu,
|
||||
handleClickCanvas,
|
||||
onEdgesChange,
|
||||
handleNodeClick,
|
||||
OssNodeTypes,
|
||||
showGrid
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -280,6 +336,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
onFitView={handleFitView}
|
||||
onCreate={handleCreateOperation}
|
||||
onDelete={handleDeleteSelected}
|
||||
onEdit={() => handleEditOperation(controller.selected[0])}
|
||||
onExecute={handleExecuteSelected}
|
||||
onResetPositions={handleResetPositions}
|
||||
onSavePositions={handleSavePositions}
|
||||
onSaveImage={handleSaveImage}
|
||||
|
@ -293,6 +351,9 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|||
onHide={handleContextMenuHide}
|
||||
onDelete={handleDeleteOperation}
|
||||
onCreateInput={handleCreateInput}
|
||||
onEditSchema={handleEditSchema}
|
||||
onEditOperation={handleEditOperation}
|
||||
onExecuteOperation={handleExecuteOperation}
|
||||
{...menuProps}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
IconAnimation,
|
||||
IconAnimationOff,
|
||||
IconDestroy,
|
||||
IconEdit2,
|
||||
IconExecute,
|
||||
IconFitImage,
|
||||
IconGrid,
|
||||
IconImage,
|
||||
|
@ -16,6 +21,7 @@ import {
|
|||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { OperationType } from '@/models/oss';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/labels';
|
||||
|
||||
|
@ -28,6 +34,8 @@ interface ToolbarOssGraphProps {
|
|||
edgeStraight: boolean;
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
onExecute: () => void;
|
||||
onFitView: () => void;
|
||||
onSaveImage: () => void;
|
||||
onSavePositions: () => void;
|
||||
|
@ -44,6 +52,8 @@ function ToolbarOssGraph({
|
|||
edgeStraight,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onExecute,
|
||||
onFitView,
|
||||
onSaveImage,
|
||||
onSavePositions,
|
||||
|
@ -53,10 +63,40 @@ function ToolbarOssGraph({
|
|||
toggleEdgeStraight
|
||||
}: ToolbarOssGraphProps) {
|
||||
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 (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='cc-icons'>
|
||||
<MiniButton
|
||||
title='Сбросить изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
disabled={!isModified}
|
||||
onClick={onResetPositions}
|
||||
/>
|
||||
<MiniButton
|
||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сбросить вид'
|
||||
|
@ -108,7 +148,6 @@ function ToolbarOssGraph({
|
|||
</div>
|
||||
{controller.isMutable ? (
|
||||
<div className='cc-icons'>
|
||||
{' '}
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
|
@ -116,19 +155,25 @@ function ToolbarOssGraph({
|
|||
onClick={onSavePositions}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Сбросить изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
disabled={!isModified}
|
||||
onClick={onResetPositions}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Новая операция'
|
||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
<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' />}
|
||||
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||
onClick={onDelete}
|
||||
|
@ -138,5 +183,5 @@ function ToolbarOssGraph({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
//IconExecute
|
||||
export default ToolbarOssGraph;
|
||||
|
|
|
@ -10,12 +10,21 @@ import { useAuth } from '@/context/AuthContext';
|
|||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||
import { AccessPolicy } from '@/models/library';
|
||||
import DlgEditOperation from '@/dialogs/DlgEditOperation';
|
||||
import { AccessPolicy, LibraryItemID } from '@/models/library';
|
||||
import { Position2D } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID } from '@/models/oss';
|
||||
import {
|
||||
IOperationCreateData,
|
||||
IOperationPosition,
|
||||
IOperationSchema,
|
||||
IOperationSetInputData,
|
||||
IOperationUpdateData,
|
||||
OperationID
|
||||
} from '@/models/oss';
|
||||
import { UserID, UserLevel } from '@/models/user';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
|
@ -45,6 +54,9 @@ export interface IOssEditContext {
|
|||
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
|
||||
deleteOperation: (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);
|
||||
|
@ -79,10 +91,17 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
|
||||
const [showEditEditors, setShowEditEditors] = useState(false);
|
||||
const [showEditLocation, setShowEditLocation] = useState(false);
|
||||
const [showEditInput, setShowEditInput] = useState(false);
|
||||
const [showEditOperation, setShowEditOperation] = useState(false);
|
||||
|
||||
const [showCreateOperation, setShowCreateOperation] = useState(false);
|
||||
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
||||
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(
|
||||
() =>
|
||||
|
@ -197,9 +216,26 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
|
||||
const handleCreateOperation = useCallback(
|
||||
(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]
|
||||
[model, positions, insertPosition]
|
||||
);
|
||||
|
||||
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(
|
||||
|
@ -221,6 +257,38 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
[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 (
|
||||
<OssEditContext.Provider
|
||||
value={{
|
||||
|
@ -246,7 +314,10 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
savePositions,
|
||||
promptCreateOperation,
|
||||
deleteOperation,
|
||||
createInput
|
||||
createInput,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
executeOperation
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
|
@ -269,11 +340,25 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
|
|||
<DlgCreateOperation
|
||||
hideWindow={() => setShowCreateOperation(false)}
|
||||
oss={model.schema}
|
||||
positions={positions}
|
||||
insertPosition={insertPosition}
|
||||
onCreate={handleCreateOperation}
|
||||
/>
|
||||
) : 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>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -646,6 +646,7 @@ export const RSEditState = ({
|
|||
) : null}
|
||||
{showSubstitute ? (
|
||||
<DlgSubstituteCst
|
||||
schema={model.schema}
|
||||
hideWindow={() => setShowSubstitute(false)} // prettier: split lines
|
||||
onSubstitute={handleSubstituteCst}
|
||||
/>
|
||||
|
|
|
@ -940,6 +940,8 @@ export const information = {
|
|||
versionDestroyed: 'Версия удалена',
|
||||
itemDestroyed: 'Схема удалена',
|
||||
operationDestroyed: 'Операция удалена',
|
||||
operationExecuted: 'Операция выполнена',
|
||||
allOperationExecuted: 'Все операции выполнены',
|
||||
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
|
||||
};
|
||||
|
||||
|
@ -949,7 +951,8 @@ export const information = {
|
|||
export const errors = {
|
||||
astFailed: 'Невозможно построить дерево разбора',
|
||||
passwordsMismatch: 'Пароли не совпадают',
|
||||
imageFailed: 'Ошибка при создании изображения'
|
||||
imageFailed: 'Ошибка при создании изображения',
|
||||
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue
Block a user