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