Compare commits
9 Commits
f91f42ff5b
...
338ad2bb98
Author | SHA1 | Date | |
---|---|---|---|
![]() |
338ad2bb98 | ||
![]() |
b1491ccd35 | ||
![]() |
8977c0fadc | ||
![]() |
f277ce288b | ||
![]() |
c91ff51afa | ||
![]() |
fbaf17ea58 | ||
![]() |
b9ab054a00 | ||
![]() |
7b39b76498 | ||
![]() |
286abaf476 |
|
@ -47,6 +47,7 @@ cover/
|
|||
# Django
|
||||
rsconcept/frontend/static
|
||||
rsconcept/frontend/media
|
||||
visualizeDB.dot
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -42,6 +42,7 @@ cover/
|
|||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
visualizeDB.dot
|
||||
|
||||
|
||||
# React
|
||||
|
|
25
.vscode/extensions.json
vendored
Normal file
25
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"jgclark.vscode-todo-highlight",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"alexcvzz.vscode-sqlite",
|
||||
"fractalbrew.backticks",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"streetsidesoftware.code-spell-checker-russian",
|
||||
"kamikillerto.vscode-colorize",
|
||||
"batisteo.vscode-django",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"seyyedkhandon.firacode",
|
||||
"ms-python.isort",
|
||||
"ms-vscode.powershell",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-python.python",
|
||||
"ms-python.debugpy",
|
||||
"ms-python.pylint",
|
||||
"ms-python.autopep8",
|
||||
"ms-python.vscode-pylance",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
|
@ -5,6 +5,7 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Run Frontend + Backend with current Database
|
||||
"name": "Run",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
|
@ -12,6 +13,7 @@
|
|||
"args": []
|
||||
},
|
||||
{
|
||||
// Run Linters
|
||||
"name": "Lint",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
|
@ -19,6 +21,7 @@
|
|||
"args": []
|
||||
},
|
||||
{
|
||||
// Run Tests
|
||||
"name": "Test",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
|
@ -26,6 +29,7 @@
|
|||
"args": []
|
||||
},
|
||||
{
|
||||
// Run Tests for backend for current file in Debug mode
|
||||
"name": "BE-DebugTestFile",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
|
@ -35,6 +39,7 @@
|
|||
"django": true
|
||||
},
|
||||
{
|
||||
// Run Tests for frontned in Debug mode
|
||||
"name": "FE-DebugTestAll",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
@ -55,6 +60,7 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
// Run Browser in Debug mode (Backend should be running)
|
||||
"name": "FE-Debug",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
|
@ -62,6 +68,7 @@
|
|||
"webRoot": "${workspaceFolder}/rsconcept/frontend"
|
||||
},
|
||||
{
|
||||
// Run Backend in Debug mode
|
||||
"name": "BE-Debug",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
|
@ -70,6 +77,7 @@
|
|||
"django": true
|
||||
},
|
||||
{
|
||||
// Run Backend test coverage
|
||||
"name": "BE-Coverage",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
|
@ -77,11 +85,20 @@
|
|||
"args": []
|
||||
},
|
||||
{
|
||||
// Recreate database, fill with initial data and Run Backend + Frontend
|
||||
"name": "Restart",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/scripts/dev/RunServer.ps1",
|
||||
"args": ["-freshStart"]
|
||||
},
|
||||
{
|
||||
// Create DOT file for visualizing database
|
||||
"name": "BE-GraphDB",
|
||||
"type": "PowerShell",
|
||||
"request": "launch",
|
||||
"script": "${workspaceFolder}/scripts/dev/GraphDB.ps1",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ This readme file is used mostly to document project dependencies
|
|||
|
||||
## ✨ Frontend [Vite + React + Typescript]
|
||||
|
||||
- to regenerate parsers use 'npm run generate' script
|
||||
|
||||
<details>
|
||||
<summary>npm install</summary>
|
||||
<pre>
|
||||
|
@ -36,10 +38,12 @@ This readme file is used mostly to document project dependencies
|
|||
- react-error-boundary
|
||||
- react-pdf
|
||||
- react-tooltip
|
||||
- reactflow
|
||||
- js-file-download
|
||||
- use-debounce
|
||||
- framer-motion
|
||||
- reagraph
|
||||
- html-to-image
|
||||
- @tanstack/react-table
|
||||
- @uiw/react-codemirror
|
||||
- @uiw/codemirror-themes
|
||||
|
@ -54,6 +58,7 @@ This readme file is used mostly to document project dependencies
|
|||
- autoprefixer
|
||||
- eslint-plugin-simple-import-sort
|
||||
- eslint-plugin-tsdoc
|
||||
- vite
|
||||
- jest
|
||||
- ts-jest
|
||||
- @types/jest
|
||||
|
@ -65,11 +70,13 @@ This readme file is used mostly to document project dependencies
|
|||
<pre>
|
||||
- ESLint
|
||||
- Colorize
|
||||
- Tailwind CSS IntelliSense
|
||||
- Code Spell Checker (eng + rus)
|
||||
- Backticks
|
||||
- Svg Preview
|
||||
- TODO Highlight v2
|
||||
- Prettier
|
||||
- PowerShell (for Windows dev env)
|
||||
</pre>
|
||||
</details>
|
||||
<details>
|
||||
|
|
|
@ -7,8 +7,24 @@ from . import models
|
|||
class OperationAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation. '''
|
||||
ordering = ['oss']
|
||||
list_display = ['oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
|
||||
search_fields = ['operation_type', 'title', 'alias']
|
||||
list_display = ['id', 'oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
|
||||
search_fields = ['id', 'operation_type', 'title', 'alias']
|
||||
|
||||
|
||||
class ArgumentAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation arguments. '''
|
||||
ordering = ['operation']
|
||||
list_display = ['id', 'operation', 'argument']
|
||||
search_fields = ['id', 'operation', 'argument']
|
||||
|
||||
|
||||
class SynthesisSubstitutionAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Substitutions as part of Synthesis operation. '''
|
||||
ordering = ['operation']
|
||||
list_display = ['id', 'operation', 'original', 'substitution', 'transfer_term']
|
||||
search_fields = ['id', 'operation', 'original', 'substitution']
|
||||
|
||||
|
||||
admin.site.register(models.Operation, OperationAdmin)
|
||||
admin.site.register(models.Argument, ArgumentAdmin)
|
||||
admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# Generated by Django 5.0.7 on 2024-07-22 13:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0001_initial'),
|
||||
('rsform', '0008_alter_libraryitem_item_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OperationSchema',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('rsform.libraryitem',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='operation',
|
||||
name='oss',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='oss.operationschema', verbose_name='Схема синтеза'),
|
||||
),
|
||||
]
|
|
@ -24,4 +24,4 @@ class Argument(Model):
|
|||
unique_together = [['operation', 'argument']]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.argument.pk} -> {self.operation.pk}'
|
||||
return f'{self.argument} -> {self.operation}'
|
||||
|
|
|
@ -21,7 +21,7 @@ class Operation(Model):
|
|||
''' Operational schema Unit.'''
|
||||
oss: ForeignKey = ForeignKey(
|
||||
verbose_name='Схема синтеза',
|
||||
to='rsform.LibraryItem',
|
||||
to='oss.OperationSchema',
|
||||
on_delete=CASCADE,
|
||||
related_name='items'
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Optional
|
|||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import Manager, QuerySet
|
||||
|
||||
from apps.rsform.models import LibraryItem, LibraryItemType
|
||||
from shared import messages as msg
|
||||
|
@ -13,30 +13,37 @@ from .Operation import Operation
|
|||
from .SynthesisSubstitution import SynthesisSubstitution
|
||||
|
||||
|
||||
class OperationSchema:
|
||||
class OperationSchema(LibraryItem):
|
||||
''' Operations schema API. '''
|
||||
|
||||
def __init__(self, item: LibraryItem):
|
||||
if item.item_type != LibraryItemType.OPERATION_SCHEMA:
|
||||
raise ValueError(msg.libraryTypeUnexpected())
|
||||
self.item = item
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
proxy = True
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'OperationSchema':
|
||||
item = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
|
||||
return OperationSchema(item=item)
|
||||
class InternalManager(Manager):
|
||||
''' Object manager. '''
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return super().get_queryset().filter(item_type=LibraryItemType.OPERATION_SCHEMA)
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs.update({'item_type': LibraryItemType.OPERATION_SCHEMA})
|
||||
return super().create(**kwargs)
|
||||
|
||||
# Legit overriding object manager
|
||||
objects = InternalManager() # type: ignore[misc]
|
||||
|
||||
def operations(self) -> QuerySet[Operation]:
|
||||
''' Get QuerySet containing all operations of current OSS. '''
|
||||
return Operation.objects.filter(oss=self.item)
|
||||
return Operation.objects.filter(oss=self)
|
||||
|
||||
def arguments(self) -> QuerySet[Argument]:
|
||||
''' Operation arguments. '''
|
||||
return Argument.objects.filter(operation__oss=self.item)
|
||||
return Argument.objects.filter(operation__oss=self)
|
||||
|
||||
def substitutions(self) -> QuerySet[SynthesisSubstitution]:
|
||||
''' Operation substitutions. '''
|
||||
return SynthesisSubstitution.objects.filter(operation__oss=self.item)
|
||||
return SynthesisSubstitution.objects.filter(operation__oss=self)
|
||||
|
||||
def update_positions(self, data: list[dict]):
|
||||
''' Update positions. '''
|
||||
|
@ -53,11 +60,8 @@ class OperationSchema:
|
|||
''' Insert new operation. '''
|
||||
if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists():
|
||||
raise ValidationError(msg.aliasTaken(kwargs['alias']))
|
||||
result = Operation.objects.create(
|
||||
oss=self.item,
|
||||
**kwargs
|
||||
)
|
||||
self.item.save()
|
||||
result = Operation.objects.create(oss=self, **kwargs)
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
|
@ -69,7 +73,7 @@ class OperationSchema:
|
|||
# deal with attached schema
|
||||
# trigger on_change effects
|
||||
|
||||
self.item.save()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_input(self, target: Operation, schema: Optional[LibraryItem]):
|
||||
|
@ -87,7 +91,7 @@ class OperationSchema:
|
|||
|
||||
# trigger on_change effects
|
||||
|
||||
self.item.save()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]:
|
||||
|
@ -95,7 +99,7 @@ class OperationSchema:
|
|||
if Argument.objects.filter(operation=operation, argument=argument).exists():
|
||||
return None
|
||||
result = Argument.objects.create(operation=operation, argument=argument)
|
||||
self.item.save()
|
||||
self.save()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -109,7 +113,7 @@ class OperationSchema:
|
|||
|
||||
# trigger on_change effects
|
||||
|
||||
self.item.save()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def set_substitutions(self, target: Operation, substitutes: list[dict]):
|
||||
|
@ -125,4 +129,4 @@ class OperationSchema:
|
|||
|
||||
# trigger on_change effects
|
||||
|
||||
self.item.save()
|
||||
self.save()
|
|
@ -33,4 +33,4 @@ class SynthesisSubstitution(Model):
|
|||
verbose_name_plural = 'Таблицы отождествлений'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.original.pk} -> {self.substitution.pk}'
|
||||
return f'{self.original} -> {self.substitution}'
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from apps.rsform.models import LibraryItem, LibraryItemType
|
||||
|
||||
from .api_OSS import OperationSchema
|
||||
from .Argument import Argument
|
||||
from .Operation import Operation, OperationType
|
||||
from .OperationSchema import OperationSchema
|
||||
from .SynthesisSubstitution import SynthesisSubstitution
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from apps.rsform.serializers import LibraryItemSerializer
|
||||
|
||||
from .basics import OperationPositionSerializer, PositionsSerializer
|
||||
from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
|
||||
from .data_access import (
|
||||
ArgumentSerializer,
|
||||
OperationCreateSerializer,
|
||||
|
|
|
@ -14,3 +14,15 @@ class PositionsSerializer(serializers.Serializer):
|
|||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer()
|
||||
)
|
||||
|
||||
|
||||
class SubstitutionExSerializer(serializers.Serializer):
|
||||
''' Serializer: Substitution extended data. '''
|
||||
operation = serializers.IntegerField()
|
||||
original = serializers.IntegerField()
|
||||
substitution = serializers.IntegerField()
|
||||
transfer_term = serializers.BooleanField()
|
||||
original_alias = serializers.CharField()
|
||||
original_term = serializers.CharField()
|
||||
substitution_alias = serializers.CharField()
|
||||
substitution_term = serializers.CharField()
|
||||
|
|
|
@ -10,7 +10,7 @@ from apps.rsform.serializers import LibraryItemDetailsSerializer
|
|||
from shared import messages as msg
|
||||
|
||||
from ..models import Argument, Operation, OperationSchema, OperationType
|
||||
from .basics import OperationPositionSerializer
|
||||
from .basics import OperationPositionSerializer, SubstitutionExSerializer
|
||||
|
||||
|
||||
class OperationSerializer(serializers.ModelSerializer):
|
||||
|
@ -42,9 +42,10 @@ class OperationCreateSerializer(serializers.Serializer):
|
|||
model = Operation
|
||||
fields = \
|
||||
'alias', 'operation_type', 'title', \
|
||||
'comment', 'position_x', 'position_y'
|
||||
'comment', 'result', 'position_x', 'position_y'
|
||||
|
||||
item_data = OperationData()
|
||||
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
|
||||
positions = serializers.ListField(
|
||||
child=OperationPositionSerializer(),
|
||||
default=[]
|
||||
|
@ -75,30 +76,32 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
|
|||
items = serializers.ListField(
|
||||
child=OperationSerializer()
|
||||
)
|
||||
graph = serializers.ListField(
|
||||
arguments = serializers.ListField(
|
||||
child=ArgumentSerializer()
|
||||
)
|
||||
substitutions = serializers.ListField(
|
||||
child=SubstitutionExSerializer()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = LibraryItem
|
||||
model = OperationSchema
|
||||
fields = '__all__'
|
||||
|
||||
def to_representation(self, instance: LibraryItem):
|
||||
def to_representation(self, instance: OperationSchema):
|
||||
result = LibraryItemDetailsSerializer(instance).data
|
||||
oss = OperationSchema(instance)
|
||||
result['items'] = []
|
||||
for operation in oss.operations():
|
||||
for operation in instance.operations():
|
||||
result['items'].append(OperationSerializer(operation).data)
|
||||
result['graph'] = []
|
||||
for argument in oss.arguments():
|
||||
result['graph'].append(ArgumentSerializer(argument).data)
|
||||
result['arguments'] = []
|
||||
for argument in instance.arguments():
|
||||
result['arguments'].append(ArgumentSerializer(argument).data)
|
||||
result['substitutions'] = []
|
||||
for substitution in oss.substitutions().values(
|
||||
for substitution in instance.substitutions().values(
|
||||
'operation',
|
||||
'original',
|
||||
'transfer_term',
|
||||
'substitution',
|
||||
'transfer_term',
|
||||
original_alias=F('original__alias'),
|
||||
original_term=F('original__term_resolved'),
|
||||
substitution_alias=F('substitution__alias'),
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
''' Tests for Django Models. '''
|
||||
from .t_Argument import *
|
||||
from .t_Operation import *
|
||||
from .t_SynthesisSubstitution import *
|
||||
|
|
36
rsconcept/backend/apps/oss/tests/s_models/t_Argument.py
Normal file
36
rsconcept/backend/apps/oss/tests/s_models/t_Argument.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
''' Testing models: Argument. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import Argument, Operation, OperationSchema, OperationType
|
||||
|
||||
|
||||
class TestArgument(TestCase):
|
||||
''' Testing Argument model. '''
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.objects.create(alias='T1')
|
||||
|
||||
self.operation1 = Operation.objects.create(oss=self.oss, alias='KS1', operation_type=OperationType.INPUT)
|
||||
self.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS)
|
||||
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT)
|
||||
self.argument = Argument.objects.create(
|
||||
operation=self.operation2,
|
||||
argument=self.operation1
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
testStr = f'{self.operation1} -> {self.operation2}'
|
||||
self.assertEqual(str(self.argument), testStr)
|
||||
|
||||
|
||||
def test_cascade_delete_operation(self):
|
||||
self.assertEqual(Argument.objects.count(), 1)
|
||||
self.operation2.delete()
|
||||
self.assertEqual(Argument.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_argument(self):
|
||||
self.assertEqual(Argument.objects.count(), 1)
|
||||
self.operation1.delete()
|
||||
self.assertEqual(Argument.objects.count(), 0)
|
31
rsconcept/backend/apps/oss/tests/s_models/t_Operation.py
Normal file
31
rsconcept/backend/apps/oss/tests/s_models/t_Operation.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
''' Testing models: Operation. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
|
||||
|
||||
class TestOperation(TestCase):
|
||||
''' Testing Operation model. '''
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.objects.create(alias='T1')
|
||||
self.operation = Operation.objects.create(
|
||||
oss=self.oss,
|
||||
alias='KS1'
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
testStr = 'Операция KS1'
|
||||
self.assertEqual(str(self.operation), testStr)
|
||||
|
||||
|
||||
def test_create_default(self):
|
||||
self.assertEqual(self.operation.oss, self.oss)
|
||||
self.assertEqual(self.operation.operation_type, OperationType.INPUT)
|
||||
self.assertEqual(self.operation.result, None)
|
||||
self.assertEqual(self.operation.alias, 'KS1')
|
||||
self.assertEqual(self.operation.title, '')
|
||||
self.assertEqual(self.operation.comment, '')
|
||||
self.assertEqual(self.operation.position_x, 0)
|
||||
self.assertEqual(self.operation.position_y, 0)
|
|
@ -0,0 +1,75 @@
|
|||
''' Testing models: SynthesisSubstitution. '''
|
||||
from unittest import result
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import (
|
||||
Argument,
|
||||
Operation,
|
||||
OperationSchema,
|
||||
OperationType,
|
||||
SynthesisSubstitution
|
||||
)
|
||||
from apps.rsform.models import RSForm
|
||||
|
||||
|
||||
class TestSynthesisSubstitution(TestCase):
|
||||
''' Testing SynthesisSubstitution model. '''
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.objects.create(alias='T1')
|
||||
|
||||
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1')
|
||||
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
||||
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2')
|
||||
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||
|
||||
self.operation1 = Operation.objects.create(
|
||||
oss=self.oss,
|
||||
alias='KS1',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1)
|
||||
self.operation2 = Operation.objects.create(
|
||||
oss=self.oss,
|
||||
alias='KS2',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1)
|
||||
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.SYNTHESIS)
|
||||
Argument.objects.create(
|
||||
operation=self.operation3,
|
||||
argument=self.operation1
|
||||
)
|
||||
Argument.objects.create(
|
||||
operation=self.operation3,
|
||||
argument=self.operation2
|
||||
)
|
||||
|
||||
self.substitution = SynthesisSubstitution.objects.create(
|
||||
operation=self.operation3,
|
||||
original=self.ks1x1,
|
||||
substitution=self.ks2x1,
|
||||
transfer_term=False
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
testStr = f'{self.ks1x1} -> {self.ks2x1}'
|
||||
self.assertEqual(str(self.substitution), testStr)
|
||||
|
||||
|
||||
def test_cascade_delete_operation(self):
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||
self.operation3.delete()
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_original(self):
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||
self.ks1x1.delete()
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_substitution(self):
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||
self.ks2x1.delete()
|
||||
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
|
@ -12,29 +12,29 @@ class TestOssViewset(EndpointTester):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user)
|
||||
self.owned_id = self.owned.item.pk
|
||||
self.unowned = OperationSchema.create(title='Test2', alias='T2')
|
||||
self.unowned_id = self.unowned.item.pk
|
||||
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
|
||||
self.private_id = self.private.item.pk
|
||||
self.invalid_id = self.private.item.pk + 1337
|
||||
self.owned = OperationSchema.objects.create(title='Test', alias='T1', owner=self.user)
|
||||
self.owned_id = self.owned.pk
|
||||
self.unowned = OperationSchema.objects.create(title='Test2', alias='T2')
|
||||
self.unowned_id = self.unowned.pk
|
||||
self.private = OperationSchema.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
|
||||
self.private_id = self.private.pk
|
||||
self.invalid_id = self.private.pk + 1337
|
||||
|
||||
|
||||
def populateData(self):
|
||||
self.ks1 = RSForm.create(alias='KS1', title='Test1')
|
||||
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1')
|
||||
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
||||
self.ks2 = RSForm.create(alias='KS2', title='Test2')
|
||||
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2')
|
||||
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||
self.operation1 = self.owned.create_operation(
|
||||
alias='1',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1.item
|
||||
result=self.ks1
|
||||
)
|
||||
self.operation2 = self.owned.create_operation(
|
||||
alias='2',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks2.item
|
||||
result=self.ks2
|
||||
)
|
||||
self.operation3 = self.owned.create_operation(
|
||||
alias='3',
|
||||
|
@ -53,12 +53,12 @@ class TestOssViewset(EndpointTester):
|
|||
self.populateData()
|
||||
|
||||
response = self.executeOK(item=self.owned_id)
|
||||
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.item.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.item.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.item.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.item.visible)
|
||||
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.visible)
|
||||
|
||||
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA)
|
||||
|
||||
|
@ -77,12 +77,12 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(sub['substitution_alias'], self.ks2x1.alias)
|
||||
self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved)
|
||||
|
||||
graph = response.data['graph']
|
||||
self.assertEqual(len(graph), 2)
|
||||
self.assertEqual(graph[0]['operation'], self.operation3.pk)
|
||||
self.assertEqual(graph[0]['argument'], self.operation1.pk)
|
||||
self.assertEqual(graph[1]['operation'], self.operation3.pk)
|
||||
self.assertEqual(graph[1]['argument'], self.operation2.pk)
|
||||
arguments = response.data['arguments']
|
||||
self.assertEqual(len(arguments), 2)
|
||||
self.assertEqual(arguments[0]['operation'], self.operation3.pk)
|
||||
self.assertEqual(arguments[0]['argument'], self.operation1.pk)
|
||||
self.assertEqual(arguments[1]['operation'], self.operation3.pk)
|
||||
self.assertEqual(arguments[1]['argument'], self.operation2.pk)
|
||||
|
||||
self.executeOK(item=self.unowned_id)
|
||||
self.executeForbidden(item=self.private_id)
|
||||
|
@ -158,6 +158,7 @@ class TestOssViewset(EndpointTester):
|
|||
self.assertEqual(new_operation['comment'], data['item_data']['comment'])
|
||||
self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
|
||||
self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
|
||||
self.assertEqual(new_operation['result'], None)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
|
||||
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
|
||||
|
@ -166,6 +167,42 @@ class TestOssViewset(EndpointTester):
|
|||
self.toggle_admin(True)
|
||||
self.executeCreated(data=data, item=self.unowned_id)
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_arguments(self):
|
||||
self.populateData()
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
'operation_type': OperationType.SYNTHESIS
|
||||
},
|
||||
'positions': [],
|
||||
'arguments': [self.operation1.pk, self.operation3.pk]
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
new_operation = response.data['new_operation']
|
||||
arguments = self.owned.arguments()
|
||||
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1))
|
||||
self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3))
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
|
||||
def test_create_operation_result(self):
|
||||
self.populateData()
|
||||
|
||||
data = {
|
||||
'item_data': {
|
||||
'alias': 'Test4',
|
||||
'operation_type': OperationType.INPUT,
|
||||
'result': self.ks1.pk
|
||||
},
|
||||
'positions': [],
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.refresh_from_db()
|
||||
new_operation = response.data['new_operation']
|
||||
self.assertEqual(new_operation['result'], self.ks1.pk)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_operation(self):
|
||||
self.executeNotFound(item=self.invalid_id)
|
||||
|
|
|
@ -20,11 +20,11 @@ from .. import serializers as s
|
|||
@extend_schema_view()
|
||||
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
|
||||
''' Endpoint: OperationSchema. '''
|
||||
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.OPERATION_SCHEMA)
|
||||
queryset = m.OperationSchema.objects.all()
|
||||
serializer_class = s.LibraryItemSerializer
|
||||
|
||||
def _get_schema(self) -> m.OperationSchema:
|
||||
return m.OperationSchema(cast(m.LibraryItem, self.get_object()))
|
||||
return cast(m.OperationSchema, self.get_object())
|
||||
|
||||
def get_permissions(self):
|
||||
''' Determine permission class. '''
|
||||
|
@ -52,7 +52,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['get'], url_path='details')
|
||||
def details(self, request: Request, pk):
|
||||
''' Endpoint: Detailed OSS data. '''
|
||||
serializer = s.OperationSchemaSerializer(cast(m.LibraryItem, self.get_object()))
|
||||
serializer = s.OperationSchemaSerializer(self._get_schema())
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=serializer.data
|
||||
|
@ -98,13 +98,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
with transaction.atomic():
|
||||
schema.update_positions(serializer.validated_data['positions'])
|
||||
new_operation = schema.create_operation(**serializer.validated_data['item_data'])
|
||||
schema.item.refresh_from_db()
|
||||
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
|
||||
for argument in serializer.validated_data['arguments']:
|
||||
schema.add_argument(operation=new_operation, argument=argument)
|
||||
schema.refresh_from_db()
|
||||
|
||||
response = Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': s.OperationSerializer(new_operation).data,
|
||||
'oss': s.OperationSchemaSerializer(schema.item).data
|
||||
'oss': s.OperationSchemaSerializer(schema).data
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
@ -126,16 +129,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
schema = self._get_schema()
|
||||
serializer = s.OperationDeleteSerializer(
|
||||
data=request.data,
|
||||
context={'oss': schema.item}
|
||||
context={'oss': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
schema.update_positions(serializer.validated_data['positions'])
|
||||
schema.delete_operation(serializer.validated_data['target'])
|
||||
schema.item.refresh_from_db()
|
||||
schema.refresh_from_db()
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(schema.item).data
|
||||
data=s.OperationSchemaSerializer(schema).data
|
||||
)
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 5.0.7 on 2024-07-22 14:21
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rsform', '0008_alter_libraryitem_item_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RSForm',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('rsform.libraryitem',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='constituenta',
|
||||
name='schema',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='librarytemplate',
|
||||
name='lib_source',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Источник'),
|
||||
),
|
||||
]
|
|
@ -40,7 +40,7 @@ class Constituenta(Model):
|
|||
''' Constituenta is the base unit for every conceptual schema. '''
|
||||
schema: ForeignKey = ForeignKey(
|
||||
verbose_name='Концептуальная схема',
|
||||
to='rsform.LibraryItem',
|
||||
to='rsform.RSForm',
|
||||
on_delete=CASCADE
|
||||
)
|
||||
order: PositiveIntegerField = PositiveIntegerField(
|
||||
|
|
|
@ -6,7 +6,7 @@ class LibraryTemplate(Model):
|
|||
''' Template for library items and constituents. '''
|
||||
lib_source: ForeignKey = ForeignKey(
|
||||
verbose_name='Источник',
|
||||
to='rsform.LibraryItem',
|
||||
to='rsform.RSForm',
|
||||
on_delete=CASCADE,
|
||||
null=True
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ from typing import Optional, cast
|
|||
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import Manager, QuerySet
|
||||
|
||||
from shared import messages as msg
|
||||
|
||||
|
@ -28,21 +28,29 @@ from .Version import Version
|
|||
_INSERT_LAST: int = -1
|
||||
|
||||
|
||||
class RSForm:
|
||||
class RSForm(LibraryItem):
|
||||
''' RSForm is math form of conceptual schema. '''
|
||||
|
||||
def __init__(self, item: LibraryItem):
|
||||
if item.item_type != LibraryItemType.RSFORM:
|
||||
raise ValueError(msg.libraryTypeUnexpected())
|
||||
self.item = item
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
proxy = True
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'RSForm':
|
||||
return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs))
|
||||
class InternalManager(Manager):
|
||||
''' Object manager. '''
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return super().get_queryset().filter(item_type=LibraryItemType.RSFORM)
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs.update({'item_type': LibraryItemType.RSFORM})
|
||||
return super().create(**kwargs)
|
||||
|
||||
# Legit overriding object manager
|
||||
objects = InternalManager() # type: ignore[misc]
|
||||
|
||||
def constituents(self) -> QuerySet[Constituenta]:
|
||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||
return Constituenta.objects.filter(schema=self.item)
|
||||
return Constituenta.objects.filter(schema=self.pk)
|
||||
|
||||
def resolver(self) -> Resolver:
|
||||
''' Create resolver for text references based on schema terms. '''
|
||||
|
@ -98,7 +106,7 @@ class RSForm:
|
|||
''' Get maximum alias index for specific CstType. '''
|
||||
result: int = 0
|
||||
items = Constituenta.objects \
|
||||
.filter(schema=self.item, cst_type=cst_type) \
|
||||
.filter(schema=self, cst_type=cst_type) \
|
||||
.order_by('-alias') \
|
||||
.values_list('alias', flat=True)
|
||||
for alias in items:
|
||||
|
@ -150,13 +158,13 @@ class RSForm:
|
|||
cst_type = guess_type(alias)
|
||||
self._shift_positions(position, 1)
|
||||
result = Constituenta.objects.create(
|
||||
schema=self.item,
|
||||
schema=self,
|
||||
order=position,
|
||||
alias=alias,
|
||||
cst_type=cst_type,
|
||||
**kwargs
|
||||
)
|
||||
self.item.save()
|
||||
self.save()
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
|
@ -183,13 +191,13 @@ class RSForm:
|
|||
result = deepcopy(items)
|
||||
for cst in result:
|
||||
cst.pk = None
|
||||
cst.schema = self.item
|
||||
cst.schema = self
|
||||
cst.order = position
|
||||
cst.alias = mapping[cst.alias]
|
||||
cst.apply_mapping(mapping)
|
||||
cst.save()
|
||||
position = position + 1
|
||||
self.item.save()
|
||||
self.save()
|
||||
return result
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -213,7 +221,7 @@ class RSForm:
|
|||
count_moved += 1
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
self.item.save()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def delete_cst(self, listCst):
|
||||
|
@ -222,7 +230,7 @@ class RSForm:
|
|||
cst.delete()
|
||||
self._reset_order()
|
||||
self.resolve_all_text()
|
||||
self.item.save()
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def substitute(
|
||||
|
@ -296,7 +304,7 @@ class RSForm:
|
|||
def create_version(self, version: str, description: str, data) -> Version:
|
||||
''' Creates version for current state. '''
|
||||
return Version.objects.create(
|
||||
item=self.item,
|
||||
item=self,
|
||||
version=version,
|
||||
description=description,
|
||||
data=data
|
||||
|
@ -322,7 +330,7 @@ class RSForm:
|
|||
prefix = get_type_prefix(cst_type)
|
||||
for text in expressions:
|
||||
new_item = Constituenta.objects.create(
|
||||
schema=self.item,
|
||||
schema=self,
|
||||
order=position,
|
||||
alias=f'{prefix}{free_index}',
|
||||
definition_formal=text,
|
||||
|
@ -332,7 +340,7 @@ class RSForm:
|
|||
free_index = free_index + 1
|
||||
position = position + 1
|
||||
|
||||
self.item.save()
|
||||
self.save()
|
||||
return result
|
||||
|
||||
def _shift_positions(self, start: int, shift: int):
|
||||
|
@ -341,7 +349,7 @@ class RSForm:
|
|||
update_list = \
|
||||
Constituenta.objects \
|
||||
.only('id', 'order', 'schema') \
|
||||
.filter(schema=self.item, order__gte=start)
|
||||
.filter(schema=self.pk, order__gte=start)
|
||||
for cst in update_list:
|
||||
cst.order += shift
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
|
@ -1,6 +1,6 @@
|
|||
''' Django: Models. '''
|
||||
|
||||
from .api_RSForm import RSForm
|
||||
from .RSForm import RSForm
|
||||
from .Constituenta import Constituenta, CstType, _empty_forms
|
||||
from .Editor import Editor
|
||||
from .LibraryItem import (
|
||||
|
|
|
@ -109,22 +109,21 @@ class CstSerializer(serializers.ModelSerializer):
|
|||
|
||||
def update(self, instance: Constituenta, validated_data) -> Constituenta:
|
||||
data = validated_data # Note: use alias for better code readability
|
||||
schema = RSForm(instance.schema)
|
||||
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None
|
||||
term: Optional[str] = data['term_raw'] if 'term_raw' in data else None
|
||||
term_changed = 'term_forms' in data
|
||||
if definition is not None and definition != instance.definition_raw:
|
||||
data['definition_resolved'] = schema.resolver().resolve(definition)
|
||||
data['definition_resolved'] = instance.schema.resolver().resolve(definition)
|
||||
if term is not None and term != instance.term_raw:
|
||||
data['term_resolved'] = schema.resolver().resolve(term)
|
||||
data['term_resolved'] = instance.schema.resolver().resolve(term)
|
||||
if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data:
|
||||
data['term_forms'] = []
|
||||
term_changed = data['term_resolved'] != instance.term_resolved
|
||||
result: Constituenta = super().update(instance, data)
|
||||
if term_changed:
|
||||
schema.on_term_change([result.id])
|
||||
instance.schema.on_term_change([result.id])
|
||||
result.refresh_from_db()
|
||||
schema.item.save()
|
||||
instance.schema.save()
|
||||
return result
|
||||
|
||||
|
||||
|
@ -170,17 +169,16 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
model = LibraryItem
|
||||
fields = '__all__'
|
||||
|
||||
def to_representation(self, instance: LibraryItem) -> dict:
|
||||
def to_representation(self, instance: RSForm) -> dict:
|
||||
result = LibraryItemDetailsSerializer(instance).data
|
||||
schema = RSForm(instance)
|
||||
result['items'] = []
|
||||
for cst in schema.constituents().order_by('order'):
|
||||
for cst in instance.constituents().order_by('order'):
|
||||
result['items'].append(CstSerializer(cst).data)
|
||||
return result
|
||||
|
||||
def to_versioned_data(self) -> dict:
|
||||
''' Create serializable version representation without redundant data. '''
|
||||
result = self.to_representation(cast(LibraryItem, self.instance))
|
||||
result = self.to_representation(cast(RSForm, self.instance))
|
||||
del result['versions']
|
||||
del result['subscribers']
|
||||
del result['editors']
|
||||
|
@ -197,14 +195,14 @@ class RSFormSerializer(serializers.ModelSerializer):
|
|||
|
||||
def from_versioned_data(self, version: int, data: dict) -> dict:
|
||||
''' Load data from version. '''
|
||||
result = self.to_representation(cast(LibraryItem, self.instance))
|
||||
result = self.to_representation(cast(RSForm, self.instance))
|
||||
result['version'] = version
|
||||
return result | data
|
||||
|
||||
@transaction.atomic
|
||||
def restore_from_version(self, data: dict):
|
||||
''' Load data from version. '''
|
||||
schema = RSForm(cast(LibraryItem, self.instance))
|
||||
schema = cast(RSForm, self.instance)
|
||||
items: list[dict] = data['items']
|
||||
ids: list[int] = [item['id'] for item in items]
|
||||
processed: list[int] = []
|
||||
|
@ -258,13 +256,13 @@ class RSFormParseSerializer(serializers.ModelSerializer):
|
|||
model = LibraryItem
|
||||
fields = '__all__'
|
||||
|
||||
def to_representation(self, instance: LibraryItem):
|
||||
def to_representation(self, instance: RSForm):
|
||||
result = RSFormSerializer(instance).data
|
||||
return self._parse_data(result)
|
||||
|
||||
def from_versioned_data(self, version: int, data: dict) -> dict:
|
||||
''' Load data from version and parse. '''
|
||||
item = cast(LibraryItem, self.instance)
|
||||
item = cast(RSForm, self.instance)
|
||||
result = RSFormSerializer(item).from_versioned_data(version, data)
|
||||
return self._parse_data(result)
|
||||
|
||||
|
@ -283,7 +281,7 @@ class CstTargetSerializer(serializers.Serializer):
|
|||
target = PKField(many=False, queryset=Constituenta.objects.all())
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
schema = cast(RSForm, self.context['schema'])
|
||||
cst = cast(Constituenta, attrs['target'])
|
||||
if schema and cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -315,7 +313,7 @@ class CstRenameSerializer(serializers.Serializer):
|
|||
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
schema = cast(RSForm, self.context['schema'])
|
||||
cst = cast(Constituenta, attrs['target'])
|
||||
if cst.schema != schema:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -326,7 +324,7 @@ class CstRenameSerializer(serializers.Serializer):
|
|||
raise serializers.ValidationError({
|
||||
'alias': msg.renameTrivial(new_alias)
|
||||
})
|
||||
if RSForm(schema).constituents().filter(alias=new_alias).exists():
|
||||
if schema.constituents().filter(alias=new_alias).exists():
|
||||
raise serializers.ValidationError({
|
||||
'alias': msg.aliasTaken(new_alias)
|
||||
})
|
||||
|
@ -338,7 +336,7 @@ class CstListSerializer(serializers.Serializer):
|
|||
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
schema = cast(RSForm, self.context['schema'])
|
||||
if not schema:
|
||||
return attrs
|
||||
|
||||
|
@ -370,7 +368,7 @@ class CstSubstituteSerializer(serializers.Serializer):
|
|||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
schema = cast(RSForm, self.context['schema'])
|
||||
deleted = set()
|
||||
for item in attrs['substitutions']:
|
||||
original_cst = cast(Constituenta, item['original'])
|
||||
|
@ -397,8 +395,8 @@ class CstSubstituteSerializer(serializers.Serializer):
|
|||
|
||||
class InlineSynthesisSerializer(serializers.Serializer):
|
||||
''' Serializer: Inline synthesis operation input. '''
|
||||
receiver = PKField(many=False, queryset=LibraryItem.objects.all())
|
||||
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
|
||||
receiver = PKField(many=False, queryset=RSForm.objects.all())
|
||||
source = PKField(many=False, queryset=RSForm.objects.all()) # type: ignore
|
||||
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||
substitutions = serializers.ListField(
|
||||
child=CstSubstituteSerializerBase()
|
||||
|
@ -406,8 +404,8 @@ class InlineSynthesisSerializer(serializers.Serializer):
|
|||
|
||||
def validate(self, attrs):
|
||||
user = cast(User, self.context['user'])
|
||||
schema_in = cast(LibraryItem, attrs['source'])
|
||||
schema_out = cast(LibraryItem, attrs['receiver'])
|
||||
schema_in = cast(RSForm, attrs['source'])
|
||||
schema_out = cast(RSForm, attrs['receiver'])
|
||||
if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
|
||||
raise PermissionDenied({
|
||||
'message': msg.schemaNotOwned(),
|
||||
|
|
|
@ -4,7 +4,7 @@ from rest_framework import serializers
|
|||
|
||||
from shared import messages as msg
|
||||
|
||||
from ..models import Constituenta, LibraryItem, RSForm
|
||||
from ..models import Constituenta, RSForm
|
||||
from ..utils import fix_old_references
|
||||
|
||||
_CST_TYPE = 'constituenta'
|
||||
|
@ -39,9 +39,9 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
def _prepare_json_rsform(schema: RSForm) -> dict:
|
||||
return {
|
||||
'type': _TRS_TYPE,
|
||||
'title': schema.item.title,
|
||||
'alias': schema.item.alias,
|
||||
'comment': schema.item.comment,
|
||||
'title': schema.title,
|
||||
'alias': schema.alias,
|
||||
'comment': schema.comment,
|
||||
'items': [],
|
||||
'claimed': False,
|
||||
'selection': [],
|
||||
|
@ -125,7 +125,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
result['comment'] = data.get('comment', '')
|
||||
if 'id' in data:
|
||||
result['id'] = data['id']
|
||||
self.instance = RSForm(LibraryItem.objects.get(pk=result['id']))
|
||||
self.instance = RSForm.objects.get(pk=result['id'])
|
||||
return result
|
||||
|
||||
def validate(self, attrs: dict):
|
||||
|
@ -139,7 +139,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data: dict) -> RSForm:
|
||||
self.instance: RSForm = RSForm.create(
|
||||
self.instance: RSForm = RSForm.objects.create(
|
||||
owner=validated_data.get('owner', None),
|
||||
alias=validated_data['alias'],
|
||||
title=validated_data['title'],
|
||||
|
@ -149,12 +149,12 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
access_policy=validated_data['access_policy'],
|
||||
location=validated_data['location']
|
||||
)
|
||||
self.instance.item.save()
|
||||
self.instance.save()
|
||||
order = 1
|
||||
for cst_data in validated_data['items']:
|
||||
cst = Constituenta(
|
||||
alias=cst_data['alias'],
|
||||
schema=self.instance.item,
|
||||
schema=self.instance,
|
||||
order=order,
|
||||
cst_type=cst_data['cstType'],
|
||||
)
|
||||
|
@ -167,11 +167,11 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
@transaction.atomic
|
||||
def update(self, instance: RSForm, validated_data) -> RSForm:
|
||||
if 'alias' in validated_data:
|
||||
instance.item.alias = validated_data['alias']
|
||||
instance.alias = validated_data['alias']
|
||||
if 'title' in validated_data:
|
||||
instance.item.title = validated_data['title']
|
||||
instance.title = validated_data['title']
|
||||
if 'comment' in validated_data:
|
||||
instance.item.comment = validated_data['comment']
|
||||
instance.comment = validated_data['comment']
|
||||
|
||||
order = 1
|
||||
prev_constituents = instance.constituents()
|
||||
|
@ -188,7 +188,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
else:
|
||||
cst = Constituenta(
|
||||
alias=cst_data['alias'],
|
||||
schema=instance.item,
|
||||
schema=instance,
|
||||
order=order,
|
||||
cst_type=cst_data['cstType'],
|
||||
)
|
||||
|
@ -202,7 +202,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
prev_cst.delete()
|
||||
|
||||
instance.resolve_all_text()
|
||||
instance.item.save()
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -3,15 +3,15 @@ from django.db.utils import IntegrityError
|
|||
from django.forms import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.rsform.models import Constituenta, CstType, LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import Constituenta, CstType, LibraryItemType, RSForm
|
||||
|
||||
|
||||
class TestConstituenta(TestCase):
|
||||
''' Testing Constituenta model. '''
|
||||
|
||||
def setUp(self):
|
||||
self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1')
|
||||
self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2')
|
||||
self.schema1 = RSForm.objects.create(title='Test1')
|
||||
self.schema2 = RSForm.objects.create(title='Test2')
|
||||
|
||||
|
||||
def test_str(self):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
''' Testing models: Editor. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.rsform.models import Editor, LibraryItem, LibraryItemType, User
|
||||
from apps.rsform.models import Editor, LibraryItemType, RSForm, User
|
||||
|
||||
|
||||
class TestEditor(TestCase):
|
||||
|
@ -10,8 +10,7 @@ class TestEditor(TestCase):
|
|||
def setUp(self):
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.item = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
self.item = RSForm.objects.create(
|
||||
title='Test',
|
||||
alias='КС1',
|
||||
owner=self.user1
|
||||
|
|
|
@ -11,49 +11,49 @@ class TestRSForm(TestCase):
|
|||
def setUp(self):
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.schema = RSForm.create(title='Test')
|
||||
self.schema = RSForm.objects.create(title='Test')
|
||||
self.assertNotEqual(self.user1, self.user2)
|
||||
|
||||
|
||||
def test_constituents(self):
|
||||
schema1 = RSForm.create(title='Test1')
|
||||
schema2 = RSForm.create(title='Test2')
|
||||
schema1 = RSForm.objects.create(title='Test1')
|
||||
schema2 = RSForm.objects.create(title='Test2')
|
||||
self.assertFalse(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
|
||||
Constituenta.objects.create(alias='X1', schema=schema1.item, order=1)
|
||||
Constituenta.objects.create(alias='X2', schema=schema1.item, order=2)
|
||||
Constituenta.objects.create(alias='X1', schema=schema1, order=1)
|
||||
Constituenta.objects.create(alias='X2', schema=schema1, order=2)
|
||||
self.assertTrue(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
self.assertEqual(schema1.constituents().count(), 2)
|
||||
|
||||
|
||||
def test_get_max_index(self):
|
||||
schema1 = RSForm.create(title='Test1')
|
||||
Constituenta.objects.create(alias='X1', schema=schema1.item, order=1)
|
||||
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.item, order=2)
|
||||
schema1 = RSForm.objects.create(title='Test1')
|
||||
Constituenta.objects.create(alias='X1', schema=schema1, order=1)
|
||||
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1, order=2)
|
||||
self.assertEqual(schema1.get_max_index(CstType.BASE), 1)
|
||||
self.assertEqual(schema1.get_max_index(CstType.TERM), 2)
|
||||
self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0)
|
||||
|
||||
|
||||
def test_insert_at(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
x1 = schema.insert_new('X1')
|
||||
self.assertEqual(x1.order, 1)
|
||||
self.assertEqual(x1.schema, schema.item)
|
||||
self.assertEqual(x1.schema, schema)
|
||||
|
||||
x2 = schema.insert_new('X2', position=1)
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(x2.schema, schema.item)
|
||||
self.assertEqual(x2.schema, schema)
|
||||
self.assertEqual(x1.order, 2)
|
||||
|
||||
x3 = schema.insert_new('X3', position=4)
|
||||
x2.refresh_from_db()
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x3.order, 3)
|
||||
self.assertEqual(x3.schema, schema.item)
|
||||
self.assertEqual(x3.schema, schema)
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(x1.order, 2)
|
||||
|
||||
|
@ -62,7 +62,7 @@ class TestRSForm(TestCase):
|
|||
x2.refresh_from_db()
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x4.order, 3)
|
||||
self.assertEqual(x4.schema, schema.item)
|
||||
self.assertEqual(x4.schema, schema)
|
||||
self.assertEqual(x3.order, 4)
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(x1.order, 2)
|
||||
|
@ -94,11 +94,11 @@ class TestRSForm(TestCase):
|
|||
def test_insert_last(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
self.assertEqual(x1.order, 1)
|
||||
self.assertEqual(x1.schema, self.schema.item)
|
||||
self.assertEqual(x1.schema, self.schema)
|
||||
|
||||
x2 = self.schema.insert_new('X2')
|
||||
self.assertEqual(x2.order, 2)
|
||||
self.assertEqual(x2.schema, self.schema.item)
|
||||
self.assertEqual(x2.schema, self.schema)
|
||||
self.assertEqual(x1.order, 1)
|
||||
|
||||
def test_create_cst(self):
|
||||
|
|
|
@ -8,12 +8,12 @@ class TestConstituentaAPI(EndpointTester):
|
|||
|
||||
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.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
|
||||
self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
|
||||
self.cst1 = Constituenta.objects.create(
|
||||
alias='X1',
|
||||
cst_type=CstType.BASE,
|
||||
schema=self.rsform_owned.item,
|
||||
schema=self.rsform_owned,
|
||||
order=1,
|
||||
convention='Test',
|
||||
term_raw='Test1',
|
||||
|
@ -22,7 +22,7 @@ class TestConstituentaAPI(EndpointTester):
|
|||
self.cst2 = Constituenta.objects.create(
|
||||
alias='X2',
|
||||
cst_type=CstType.BASE,
|
||||
schema=self.rsform_unowned.item,
|
||||
schema=self.rsform_unowned,
|
||||
order=1,
|
||||
convention='Test1',
|
||||
term_raw='Test2',
|
||||
|
@ -30,7 +30,7 @@ class TestConstituentaAPI(EndpointTester):
|
|||
)
|
||||
self.cst3 = Constituenta.objects.create(
|
||||
alias='X3',
|
||||
schema=self.rsform_owned.item,
|
||||
schema=self.rsform_owned,
|
||||
order=2,
|
||||
term_raw='Test3',
|
||||
term_resolved='Test3',
|
||||
|
|
|
@ -20,20 +20,16 @@ class TestLibraryViewset(EndpointTester):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
self.owned = RSForm.objects.create(
|
||||
title='Test',
|
||||
alias='T1',
|
||||
owner=self.user
|
||||
)
|
||||
self.schema = RSForm(self.owned)
|
||||
self.unowned = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
self.unowned = RSForm.objects.create(
|
||||
title='Test2',
|
||||
alias='T2'
|
||||
)
|
||||
self.common = LibraryItem.objects.create(
|
||||
item_type=LibraryItemType.RSFORM,
|
||||
self.common = RSForm.objects.create(
|
||||
title='Test3',
|
||||
alias='T3',
|
||||
location=LocationHead.COMMON
|
||||
|
@ -363,12 +359,12 @@ class TestLibraryViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/library/{item}/clone', method='post')
|
||||
def test_clone_rsform(self):
|
||||
x12 = self.schema.insert_new(
|
||||
x12 = self.owned.insert_new(
|
||||
alias='X12',
|
||||
term_raw='человек',
|
||||
term_resolved='человек'
|
||||
)
|
||||
d2 = self.schema.insert_new(
|
||||
d2 = self.owned.insert_new(
|
||||
alias='D2',
|
||||
term_raw='@{X12|plur}',
|
||||
term_resolved='люди'
|
||||
|
|
|
@ -10,16 +10,16 @@ class TestInlineSynthesis(EndpointTester):
|
|||
@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')
|
||||
self.schema1 = RSForm.objects.create(title='Test1', alias='T1', owner=self.user)
|
||||
self.schema2 = RSForm.objects.create(title='Test2', alias='T2', owner=self.user)
|
||||
self.unowned = RSForm.objects.create(title='Test3', alias='T3')
|
||||
|
||||
|
||||
def test_inline_synthesis_inputs(self):
|
||||
invalid_id = 1338
|
||||
data = {
|
||||
'receiver': self.unowned.item.pk,
|
||||
'source': self.schema1.item.pk,
|
||||
'receiver': self.unowned.pk,
|
||||
'source': self.schema1.pk,
|
||||
'items': [],
|
||||
'substitutions': []
|
||||
}
|
||||
|
@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester):
|
|||
data['receiver'] = invalid_id
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['receiver'] = self.schema1.item.pk
|
||||
data['receiver'] = self.schema1.pk
|
||||
data['source'] = invalid_id
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['source'] = self.schema1.item.pk
|
||||
data['source'] = self.schema1.pk
|
||||
self.executeOK(data=data)
|
||||
|
||||
data['items'] = [invalid_id]
|
||||
|
@ -51,8 +51,8 @@ class TestInlineSynthesis(EndpointTester):
|
|||
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
|
||||
|
||||
data = {
|
||||
'receiver': self.schema1.item.pk,
|
||||
'source': self.schema2.item.pk,
|
||||
'receiver': self.schema1.pk,
|
||||
'source': self.schema2.pk,
|
||||
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
|
||||
'substitutions': [
|
||||
{
|
||||
|
|
|
@ -24,12 +24,12 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
|
||||
self.owned_id = self.owned.item.pk
|
||||
self.unowned = RSForm.create(title='Test2', alias='T2')
|
||||
self.unowned_id = self.unowned.item.pk
|
||||
self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
|
||||
self.private_id = self.private.item.pk
|
||||
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
|
||||
self.owned_id = self.owned.pk
|
||||
self.unowned = RSForm.objects.create(title='Test2', alias='T2')
|
||||
self.unowned_id = self.unowned.pk
|
||||
self.private = RSForm.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
|
||||
self.private_id = self.private.pk
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/create-detailed', method='post')
|
||||
|
@ -63,19 +63,19 @@ class TestRSFormViewset(EndpointTester):
|
|||
)
|
||||
response = self.executeOK()
|
||||
self.assertFalse(response_contains(response, non_schema))
|
||||
self.assertTrue(response_contains(response, self.unowned.item))
|
||||
self.assertTrue(response_contains(response, self.owned.item))
|
||||
self.assertTrue(response_contains(response, self.unowned))
|
||||
self.assertTrue(response_contains(response, self.owned))
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/contents', method='get')
|
||||
def test_contents(self):
|
||||
response = self.executeOK(item=self.owned_id)
|
||||
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.item.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.item.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.item.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.item.visible)
|
||||
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.visible)
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/details', method='get')
|
||||
|
@ -92,12 +92,12 @@ class TestRSFormViewset(EndpointTester):
|
|||
)
|
||||
|
||||
response = self.executeOK(item=self.owned_id)
|
||||
self.assertEqual(response.data['owner'], self.owned.item.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.item.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.item.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.item.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.item.visible)
|
||||
self.assertEqual(response.data['owner'], self.owned.owner.pk)
|
||||
self.assertEqual(response.data['title'], self.owned.title)
|
||||
self.assertEqual(response.data['alias'], self.owned.alias)
|
||||
self.assertEqual(response.data['location'], self.owned.location)
|
||||
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
|
||||
self.assertEqual(response.data['visible'], self.owned.visible)
|
||||
|
||||
self.assertEqual(len(response.data['items']), 2)
|
||||
self.assertEqual(response.data['items'][0]['id'], x1.pk)
|
||||
|
@ -176,9 +176,9 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
|
||||
def test_export_trs(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema = RSForm.objects.create(title='Test')
|
||||
schema.insert_new('X1')
|
||||
response = self.executeOK(item=schema.item.pk)
|
||||
response = self.executeOK(item=schema.pk)
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
|
||||
with io.BytesIO(response.content) as stream:
|
||||
with ZipFile(stream, 'r') as zipped_file:
|
||||
|
@ -219,6 +219,15 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.assertEqual(x4.term_raw, data['term_raw'])
|
||||
self.assertEqual(x4.term_forms, data['term_forms'])
|
||||
|
||||
data = {
|
||||
'alias': 'X5',
|
||||
'cst_type': CstType.BASE,
|
||||
'insert_after': None,
|
||||
'term_raw': 'test5'
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch')
|
||||
def test_rename_constituenta(self):
|
||||
|
@ -387,7 +396,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
data = {'items': [x1.pk]}
|
||||
response = self.executeOK(data=data)
|
||||
x2.refresh_from_db()
|
||||
self.owned.item.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(len(response.data['items']), 1)
|
||||
self.assertEqual(self.owned.constituents().count(), 1)
|
||||
self.assertEqual(x2.alias, 'X2')
|
||||
|
@ -449,16 +458,16 @@ class TestRSFormViewset(EndpointTester):
|
|||
@decl_endpoint('/api/rsforms/{item}/load-trs', method='patch')
|
||||
def test_load_trs(self):
|
||||
self.set_params(item=self.owned_id)
|
||||
self.owned.item.title = 'Test11'
|
||||
self.owned.item.save()
|
||||
self.owned.title = 'Test11'
|
||||
self.owned.save()
|
||||
x1 = self.owned.insert_new('X1')
|
||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file, 'load_metadata': False}
|
||||
response = self.client.patch(self.endpoint, data=data, format='multipart')
|
||||
self.owned.item.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(self.owned.item.title, 'Test11')
|
||||
self.assertEqual(self.owned.title, 'Test11')
|
||||
self.assertEqual(len(response.data['items']), 25)
|
||||
self.assertEqual(self.owned.constituents().count(), 25)
|
||||
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())
|
||||
|
|
|
@ -15,10 +15,9 @@ class TestVersionViews(EndpointTester):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user).item
|
||||
self.schema = RSForm(self.owned)
|
||||
self.unowned = RSForm.create(title='Test2', alias='T2').item
|
||||
self.x1 = self.schema.insert_new(
|
||||
self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
|
||||
self.unowned = RSForm.objects.create(title='Test2', alias='T2')
|
||||
self.x1 = self.owned.insert_new(
|
||||
alias='X1',
|
||||
convention='testStart'
|
||||
)
|
||||
|
@ -135,14 +134,14 @@ class TestVersionViews(EndpointTester):
|
|||
@decl_endpoint('/api/versions/{version}/restore', method='patch')
|
||||
def test_restore_version(self):
|
||||
x1 = self.x1
|
||||
x2 = self.schema.insert_new('X2')
|
||||
d1 = self.schema.insert_new('D1', term_raw='TestTerm')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
d1 = self.owned.insert_new('D1', term_raw='TestTerm')
|
||||
data = {'version': '1.0.0', 'description': 'test'}
|
||||
version_id = self._create_version(data=data)
|
||||
invalid_id = version_id + 1337
|
||||
|
||||
d1.delete()
|
||||
x3 = self.schema.insert_new('X3')
|
||||
x3 = self.owned.insert_new('X3')
|
||||
x1.order = x3.order
|
||||
x1.convention = 'Test2'
|
||||
x1.term_raw = 'Test'
|
||||
|
|
|
@ -85,7 +85,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
serializer = s.LibraryItemCloneSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
item = self._get_item()
|
||||
clone = deepcopy(item)
|
||||
if item.item_type != m.LibraryItemType.RSFORM:
|
||||
return Response(status=c.HTTP_400_BAD_REQUEST)
|
||||
|
||||
schema = m.RSForm.objects.get(pk=item.pk)
|
||||
clone = deepcopy(schema)
|
||||
clone.pk = None
|
||||
clone.owner = self.request.user
|
||||
clone.title = serializer.validated_data['title']
|
||||
|
@ -98,9 +102,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
|
||||
with transaction.atomic():
|
||||
clone.save()
|
||||
if clone.item_type == m.LibraryItemType.RSFORM:
|
||||
need_filter = 'items' in request.data
|
||||
for cst in m.RSForm(item).constituents():
|
||||
for cst in schema.constituents():
|
||||
if not need_filter or cst.pk in request.data['items']:
|
||||
cst.pk = None
|
||||
cst.schema = clone
|
||||
|
@ -109,7 +112,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
status=c.HTTP_201_CREATED,
|
||||
data=s.RSFormParseSerializer(clone).data
|
||||
)
|
||||
return Response(status=c.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary='subscribe to item',
|
||||
|
|
|
@ -27,7 +27,7 @@ def inline_synthesis(request: Request):
|
|||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
schema = m.RSForm(serializer.validated_data['receiver'])
|
||||
schema = cast(m.RSForm, serializer.validated_data['receiver'])
|
||||
items = cast(list[m.Constituenta], serializer.validated_data['items'])
|
||||
|
||||
with transaction.atomic():
|
||||
|
@ -46,5 +46,5 @@ def inline_synthesis(request: Request):
|
|||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
|
|
@ -25,11 +25,11 @@ from .. import utils
|
|||
@extend_schema_view()
|
||||
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
|
||||
''' Endpoint: RSForm operations. '''
|
||||
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM)
|
||||
queryset = m.RSForm.objects.all()
|
||||
serializer_class = s.LibraryItemSerializer
|
||||
|
||||
def _get_schema(self) -> m.RSForm:
|
||||
return m.RSForm(cast(m.LibraryItem, self.get_object()))
|
||||
return cast(m.RSForm, self.get_object())
|
||||
|
||||
def get_permissions(self):
|
||||
''' Determine permission class. '''
|
||||
|
@ -72,7 +72,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
serializer = s.CstCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
if 'insert_after' in data:
|
||||
if 'insert_after' in data and data['insert_after'] is not None:
|
||||
try:
|
||||
insert_after = m.Constituenta.objects.get(pk=data['insert_after'])
|
||||
except m.LibraryItem.DoesNotExist:
|
||||
|
@ -81,12 +81,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
insert_after = None
|
||||
new_cst = schema.create_cst(data, insert_after)
|
||||
|
||||
schema.item.refresh_from_db()
|
||||
schema.refresh_from_db()
|
||||
response = Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_cst': s.CstSerializer(new_cst).data,
|
||||
'schema': s.RSFormParseSerializer(schema.item).data
|
||||
'schema': s.RSFormParseSerializer(schema).data
|
||||
}
|
||||
)
|
||||
response['Location'] = new_cst.get_absolute_url()
|
||||
|
@ -108,11 +108,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
''' Produce a term for every element of the target constituenta typification. '''
|
||||
schema = self._get_schema()
|
||||
|
||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema.item})
|
||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||
|
||||
schema_details = s.RSFormParseSerializer(schema.item).data['items']
|
||||
schema_details = s.RSFormParseSerializer(schema).data['items']
|
||||
cst_parse = next(item for item in schema_details if item['id'] == cst.id)['parse']
|
||||
if not cst_parse['typification']:
|
||||
return Response(
|
||||
|
@ -125,7 +125,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
'cst_list': result,
|
||||
'schema': s.RSFormParseSerializer(schema.item).data
|
||||
'schema': s.RSFormParseSerializer(schema).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -144,7 +144,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
def cst_rename(self, request: Request, pk):
|
||||
''' Rename constituenta possibly changing type. '''
|
||||
schema = self._get_schema()
|
||||
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema.item})
|
||||
serializer = s.CstRenameSerializer(data=request.data, context={'schema': schema})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||
|
@ -156,14 +156,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
with transaction.atomic():
|
||||
cst.save()
|
||||
schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
|
||||
schema.item.refresh_from_db()
|
||||
schema.refresh_from_db()
|
||||
cst.refresh_from_db()
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
'new_cst': s.CstSerializer(cst).data,
|
||||
'schema': s.RSFormParseSerializer(schema.item).data
|
||||
'schema': s.RSFormParseSerializer(schema).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -184,7 +184,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema = self._get_schema()
|
||||
serializer = s.CstSubstituteSerializer(
|
||||
data=request.data,
|
||||
context={'schema': schema.item}
|
||||
context={'schema': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
@ -193,10 +193,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
original = cast(m.Constituenta, substitution['original'])
|
||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||
schema.substitute(original, replacement, substitution['transfer_term'])
|
||||
schema.item.refresh_from_db()
|
||||
schema.refresh_from_db()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -216,14 +216,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema = self._get_schema()
|
||||
serializer = s.CstListSerializer(
|
||||
data=request.data,
|
||||
context={'schema': schema.item}
|
||||
context={'schema': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.delete_cst(serializer.validated_data['items'])
|
||||
schema.item.refresh_from_db()
|
||||
schema.refresh_from_db()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -243,7 +243,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema = self._get_schema()
|
||||
serializer = s.CstMoveSerializer(
|
||||
data=request.data,
|
||||
context={'schema': schema.item}
|
||||
context={'schema': schema}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema.move_cst(
|
||||
|
@ -252,7 +252,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -272,7 +272,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema.reset_aliases()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -292,7 +292,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema.restore_order()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(schema.item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -314,7 +314,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
schema = self._get_schema()
|
||||
load_metadata = input_serializer.validated_data['load_metadata']
|
||||
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||
data['id'] = schema.item.pk
|
||||
data['id'] = schema.pk
|
||||
|
||||
serializer = s.RSFormTRSSerializer(
|
||||
data=data,
|
||||
|
@ -324,7 +324,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
result = serializer.save()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(result.item).data
|
||||
data=s.RSFormParseSerializer(result).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -357,7 +357,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['get'], url_path='details')
|
||||
def details(self, request: Request, pk):
|
||||
''' Endpoint: Detailed schema view including statuses and parse. '''
|
||||
serializer = s.RSFormParseSerializer(cast(m.LibraryItem, self.get_object()))
|
||||
serializer = s.RSFormParseSerializer(self._get_schema())
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=serializer.data
|
||||
|
@ -421,7 +421,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
''' Endpoint: Download Exteor compatible file. '''
|
||||
data = s.RSFormTRSSerializer(self._get_schema()).data
|
||||
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
||||
filename = utils.filename_for_schema(self._get_schema().item.alias)
|
||||
filename = utils.filename_for_schema(self._get_schema().alias)
|
||||
response = HttpResponse(file, content_type='application/zip')
|
||||
response['Content-Disposition'] = f'attachment; filename={filename}'
|
||||
return response
|
||||
|
@ -451,7 +451,7 @@ class TrsImportView(views.APIView):
|
|||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema = serializer.save()
|
||||
result = s.LibraryItemSerializer(schema.item)
|
||||
result = s.LibraryItemSerializer(schema)
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data=result.data
|
||||
|
@ -483,7 +483,7 @@ def create_rsform(request: Request):
|
|||
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||
serializer_rsform.is_valid(raise_exception=True)
|
||||
schema = serializer_rsform.save()
|
||||
result = s.LibraryItemSerializer(schema.item)
|
||||
result = s.LibraryItemSerializer(schema)
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data=result.data
|
||||
|
|
|
@ -42,10 +42,11 @@ class VersionViewset(
|
|||
''' Restore version data into current item. '''
|
||||
version = cast(m.Version, self.get_object())
|
||||
item = cast(m.LibraryItem, version.item)
|
||||
s.RSFormSerializer(item).restore_from_version(version.data)
|
||||
schema = m.RSForm.objects.get(pk=item.pk)
|
||||
s.RSFormSerializer(schema).restore_from_version(version.data)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(schema).data
|
||||
)
|
||||
|
||||
|
||||
|
@ -65,7 +66,7 @@ class VersionViewset(
|
|||
def create_version(request: Request, pk_item: int):
|
||||
''' Endpoint: Create new version for RSForm copying current content. '''
|
||||
try:
|
||||
item = m.LibraryItem.objects.get(pk=pk_item)
|
||||
item = m.RSForm.objects.get(pk=pk_item)
|
||||
except m.LibraryItem.DoesNotExist:
|
||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||
creator = request.user
|
||||
|
@ -75,7 +76,7 @@ def create_version(request: Request, pk_item: int):
|
|||
version_input = s.VersionCreateSerializer(data=request.data)
|
||||
version_input.is_valid(raise_exception=True)
|
||||
data = s.RSFormSerializer(item).to_versioned_data()
|
||||
result = m.RSForm(item).create_version(
|
||||
result = item.create_version(
|
||||
version=version_input.validated_data['version'],
|
||||
description=version_input.validated_data['description'],
|
||||
data=data
|
||||
|
@ -102,8 +103,8 @@ def create_version(request: Request, pk_item: int):
|
|||
def retrieve_version(request: Request, pk_item: int, pk_version: int):
|
||||
''' Endpoint: Retrieve version for RSForm. '''
|
||||
try:
|
||||
item = m.LibraryItem.objects.get(pk=pk_item)
|
||||
except m.LibraryItem.DoesNotExist:
|
||||
item = m.RSForm.objects.get(pk=pk_item)
|
||||
except m.RSForm.DoesNotExist:
|
||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||
try:
|
||||
version = m.Version.objects.get(pk=pk_version)
|
||||
|
@ -135,7 +136,8 @@ def export_file(request: Request, pk: int):
|
|||
version = m.Version.objects.get(pk=pk)
|
||||
except m.Version.DoesNotExist:
|
||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||
data = s.RSFormTRSSerializer(m.RSForm(version.item)).from_versioned_data(version.data)
|
||||
schema = m.RSForm.objects.get(pk=version.item.pk)
|
||||
data = s.RSFormTRSSerializer(schema).from_versioned_data(version.data)
|
||||
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
||||
filename = utils.filename_for_schema(data['alias'])
|
||||
response = HttpResponse(file, content_type='application/zip')
|
||||
|
|
|
@ -79,6 +79,8 @@ INSTALLED_APPS = [
|
|||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar',
|
||||
]
|
||||
if DEBUG:
|
||||
INSTALLED_APPS.append('django_extensions')
|
||||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
|
@ -128,7 +130,6 @@ APPEND_SLASH = False
|
|||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
|
||||
STATIC_URL = 'static/'
|
||||
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
|
||||
|
@ -156,7 +157,6 @@ WSGI_APPLICATION = 'project.wsgi.application'
|
|||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
|
||||
|
@ -198,7 +198,6 @@ SPECTACULAR_SETTINGS = {
|
|||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS: list[str] = [
|
||||
# NOTE: Password validators disabled
|
||||
# {
|
||||
|
@ -231,6 +230,14 @@ USE_TZ = True
|
|||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
# Graph model settings for visualization
|
||||
# https://django-extensions.readthedocs.io/en/latest/graph_models.html
|
||||
GRAPH_MODELS = {
|
||||
'all_applications': True,
|
||||
'group_models': True,
|
||||
}
|
||||
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
|
|
|
@ -14,6 +14,7 @@ psycopg2-binary
|
|||
gunicorn
|
||||
|
||||
djangorestframework-stubs[compatible-mypy]
|
||||
django-extensions
|
||||
mypy
|
||||
pylint
|
||||
coverage
|
|
@ -50,10 +50,6 @@ def typificationInvalidStr():
|
|||
return 'Invalid typification string'
|
||||
|
||||
|
||||
def libraryTypeUnexpected():
|
||||
return 'Attempting to use invalid adaptor for non-RSForm item'
|
||||
|
||||
|
||||
def exteorFileVersionNotSupported():
|
||||
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
||||
|
||||
|
|
|
@ -12,12 +12,13 @@ WORKDIR /result
|
|||
|
||||
RUN npm install -g typescript vite
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY ./ ./
|
||||
COPY ./env/.env.$BUILD_TYPE ./
|
||||
RUN rm -rf ./env
|
||||
|
||||
RUN npm ci
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
|
|
7
rsconcept/frontend/package-lock.json
generated
7
rsconcept/frontend/package-lock.json
generated
|
@ -15,6 +15,7 @@
|
|||
"axios": "^1.7.2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.8",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
@ -6808,6 +6809,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-to-image": {
|
||||
"version": "1.11.11",
|
||||
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
|
||||
"integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "lezer-generator src/components/RSInput/rslang/rslangFull.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
||||
"generate": "lezer-generator src/components/RSInput/rslang/rslangFull.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
|
||||
"test": "jest",
|
||||
"dev": "vite --host",
|
||||
"build": "tsc && vite build",
|
||||
|
@ -19,6 +19,7 @@
|
|||
"axios": "^1.7.2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.8",
|
||||
"html-to-image": "^1.11.11",
|
||||
"js-file-download": "^0.4.12",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
|
|
@ -7,17 +7,16 @@ import { toast } from 'react-toastify';
|
|||
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
|
||||
import {
|
||||
AccessPolicy,
|
||||
ILibraryItem,
|
||||
ILibraryUpdateData,
|
||||
ITargetAccessPolicy,
|
||||
ITargetLocation,
|
||||
IVersionData,
|
||||
LibraryItemType
|
||||
} from '@/models/library';
|
||||
import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library';
|
||||
import { ILibraryCreateData } from '@/models/library';
|
||||
import { IOperationSchemaData } from '@/models/oss';
|
||||
import {
|
||||
ICstSubstituteData,
|
||||
IOperationCreateData,
|
||||
IOperationCreatedResponse,
|
||||
IOperationSchemaData,
|
||||
IPositionsData,
|
||||
ITargetOperation
|
||||
} from '@/models/oss';
|
||||
import {
|
||||
IConstituentaList,
|
||||
IConstituentaMeta,
|
||||
|
@ -25,7 +24,6 @@ import {
|
|||
ICstCreatedResponse,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IInlineSynthesisData,
|
||||
IProduceStructureResponse,
|
||||
|
@ -233,30 +231,6 @@ export function postCloneLibraryItem(target: string, request: FrontExchange<IRSF
|
|||
});
|
||||
}
|
||||
|
||||
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
|
||||
request.setLoading!(false);
|
||||
request.onSuccess({
|
||||
id: Number(target),
|
||||
comment: '123',
|
||||
alias: 'oss1',
|
||||
access_policy: AccessPolicy.PUBLIC,
|
||||
editors: [],
|
||||
owner: 1,
|
||||
item_type: LibraryItemType.OSS,
|
||||
location: '/U',
|
||||
read_only: false,
|
||||
subscribers: [],
|
||||
time_create: '0',
|
||||
time_update: '0',
|
||||
title: 'TestOss',
|
||||
visible: false
|
||||
});
|
||||
// AxiosGet({
|
||||
// endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS
|
||||
// request: request
|
||||
// });
|
||||
}
|
||||
|
||||
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
|
||||
if (!version) {
|
||||
AxiosGet({
|
||||
|
@ -357,7 +331,7 @@ export function getTRSFile(target: string, version: string, request: FrontPull<B
|
|||
}
|
||||
}
|
||||
|
||||
export function postNewConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
|
||||
export function postCreateConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
|
||||
AxiosPost({
|
||||
endpoint: `/api/rsforms/${schema}/cst-create`,
|
||||
request: request
|
||||
|
@ -445,6 +419,37 @@ export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData
|
|||
});
|
||||
}
|
||||
|
||||
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
|
||||
AxiosGet({
|
||||
endpoint: `/api/oss/${target}/details`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/oss/${schema}/update-positions`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postCreateOperation(
|
||||
schema: string,
|
||||
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
|
||||
) {
|
||||
AxiosPost({
|
||||
endpoint: `/api/oss/${schema}/create-operation`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
|
||||
AxiosPatch({
|
||||
endpoint: `/api/oss/${schema}/delete-operation`,
|
||||
request: request
|
||||
});
|
||||
}
|
||||
|
||||
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
|
||||
AxiosPost({
|
||||
endpoint: `/api/cctext/inflect`,
|
||||
|
|
|
@ -38,6 +38,7 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
|
|||
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
|
||||
export { LuLightbulb as IconHelp } from 'react-icons/lu';
|
||||
export { LuLightbulbOff as IconHelpOff } from 'react-icons/lu';
|
||||
export { TbGridDots as IconGrid } from 'react-icons/tb';
|
||||
export { RiPushpinFill as IconPin } from 'react-icons/ri';
|
||||
export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
|
||||
export { BiCaretDown as IconSortDesc } from 'react-icons/bi';
|
||||
|
|
|
@ -8,7 +8,7 @@ import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
|
|||
import Label from '@/components/ui/Label';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { IConstituenta, IRSForm, ISubstitution } from '@/models/rsform';
|
||||
import { IConstituenta, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
import { describeConstituenta } from '@/utils/labels';
|
||||
|
||||
import {
|
||||
|
@ -34,11 +34,11 @@ interface PickSubstitutionsProps {
|
|||
filter1?: (cst: IConstituenta) => boolean;
|
||||
filter2?: (cst: IConstituenta) => boolean;
|
||||
|
||||
items: ISubstitution[];
|
||||
setItems: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
|
||||
items: ISingleSubstitution[];
|
||||
setItems: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
|
||||
}
|
||||
|
||||
function SubstitutionIcon({ item }: { item: ISubstitution }) {
|
||||
function SubstitutionIcon({ item }: { item: ISingleSubstitution }) {
|
||||
if (item.deleteRight) {
|
||||
if (item.takeLeftTerm) {
|
||||
return <IconPageRight size='1.2rem' />;
|
||||
|
@ -54,7 +54,7 @@ function SubstitutionIcon({ item }: { item: ISubstitution }) {
|
|||
}
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ISubstitution>();
|
||||
const columnHelper = createColumnHelper<ISingleSubstitution>();
|
||||
|
||||
function PickSubstitutions({
|
||||
items,
|
||||
|
@ -80,7 +80,7 @@ function PickSubstitutions({
|
|||
if (!leftCst || !rightCst) {
|
||||
return;
|
||||
}
|
||||
const newSubstitution: ISubstitution = {
|
||||
const newSubstitution: ISingleSubstitution = {
|
||||
leftCst: leftCst,
|
||||
rightCst: rightCst,
|
||||
deleteRight: deleteRight,
|
||||
|
@ -99,7 +99,7 @@ function PickSubstitutions({
|
|||
const handleDeleteRow = useCallback(
|
||||
(row: number) => {
|
||||
setItems(prev => {
|
||||
const newItems: ISubstitution[] = [];
|
||||
const newItems: ISingleSubstitution[] = [];
|
||||
prev.forEach((item, index) => {
|
||||
if (index !== row) {
|
||||
newItems.push(item);
|
||||
|
|
58
rsconcept/frontend/src/components/select/SelectOperation.tsx
Normal file
58
rsconcept/frontend/src/components/select/SelectOperation.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { IOperation, OperationID } from '@/models/oss';
|
||||
import { matchOperation } from '@/models/ossAPI';
|
||||
|
||||
import { CProps } from '../props';
|
||||
import SelectSingle from '../ui/SelectSingle';
|
||||
|
||||
interface SelectOperationProps extends CProps.Styling {
|
||||
items?: IOperation[];
|
||||
value?: IOperation;
|
||||
onSelectValue: (newValue?: IOperation) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function SelectOperation({
|
||||
className,
|
||||
items,
|
||||
value,
|
||||
onSelectValue,
|
||||
placeholder = 'Выберите операцию',
|
||||
...restProps
|
||||
}: SelectOperationProps) {
|
||||
const options = useMemo(() => {
|
||||
return (
|
||||
items?.map(cst => ({
|
||||
value: cst.id,
|
||||
label: `${cst.alias}: ${cst.title}`
|
||||
})) ?? []
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const filter = useCallback(
|
||||
(option: { value: OperationID | undefined; label: string }, inputValue: string) => {
|
||||
const operation = items?.find(item => item.id === option.value);
|
||||
return !operation ? false : matchOperation(operation, inputValue);
|
||||
},
|
||||
[items]
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectSingle
|
||||
className={clsx('text-ellipsis', className)}
|
||||
options={options}
|
||||
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : undefined}
|
||||
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 SelectOperation;
|
|
@ -25,8 +25,8 @@ function TextArea({
|
|||
<div
|
||||
className={clsx(
|
||||
{
|
||||
'flex flex-col gap-2': !dense,
|
||||
'flex items-center gap-3': dense
|
||||
'flex flex-col flex-grow gap-2': !dense,
|
||||
'flex flex-grow items-center gap-3': dense
|
||||
},
|
||||
dense && className
|
||||
)}
|
||||
|
|
|
@ -5,18 +5,21 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react
|
|||
import {
|
||||
type DataCallback,
|
||||
deleteUnsubscribe,
|
||||
patchDeleteOperation,
|
||||
patchEditorsSet as patchSetEditors,
|
||||
patchLibraryItem,
|
||||
patchSetAccessPolicy,
|
||||
patchSetLocation,
|
||||
patchSetOwner,
|
||||
patchUpdatePositions,
|
||||
postCreateOperation,
|
||||
postSubscribe
|
||||
} from '@/app/backendAPI';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import useOssDetails from '@/hooks/useOssDetails';
|
||||
import { AccessPolicy, ILibraryItem } from '@/models/library';
|
||||
import { ILibraryUpdateData } from '@/models/library';
|
||||
import { IOperationSchema } from '@/models/oss';
|
||||
import { IOperation, IOperationCreateData, IOperationSchema, IPositionsData, ITargetOperation } from '@/models/oss';
|
||||
import { UserID } from '@/models/user';
|
||||
import { contextOutsideScope } from '@/utils/labels';
|
||||
|
||||
|
@ -43,6 +46,10 @@ interface IOssContext {
|
|||
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
|
||||
setLocation: (newLocation: string, callback?: () => void) => void;
|
||||
setEditors: (newEditors: UserID[], callback?: () => void) => void;
|
||||
|
||||
savePositions: (data: IPositionsData, callback?: () => void) => void;
|
||||
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
|
||||
deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
|
||||
}
|
||||
|
||||
const OssContext = createContext<IOssContext | null>(null);
|
||||
|
@ -63,13 +70,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
const library = useLibrary();
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
schema: schema, // prettier: split lines
|
||||
schema, // prettier: split lines
|
||||
error: errorLoading,
|
||||
setSchema,
|
||||
loading
|
||||
} = useOssDetails({
|
||||
target: itemID
|
||||
});
|
||||
} = useOssDetails({ target: itemID });
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
|
||||
|
||||
|
@ -249,6 +254,59 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
[itemID, schema]
|
||||
);
|
||||
|
||||
const savePositions = useCallback(
|
||||
(data: IPositionsData, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchUpdatePositions(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: () => {
|
||||
library.localUpdateTimestamp(Number(itemID));
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
[itemID, library]
|
||||
);
|
||||
|
||||
const createOperation = useCallback(
|
||||
(data: IOperationCreateData, callback?: DataCallback<IOperation>) => {
|
||||
setProcessingError(undefined);
|
||||
postCreateOperation(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData.oss);
|
||||
library.localUpdateTimestamp(newData.oss.id);
|
||||
if (callback) callback(newData.new_operation);
|
||||
}
|
||||
});
|
||||
},
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
const deleteOperation = useCallback(
|
||||
(data: ITargetOperation, callback?: () => void) => {
|
||||
setProcessingError(undefined);
|
||||
patchDeleteOperation(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: setProcessingError,
|
||||
onSuccess: newData => {
|
||||
setSchema(newData);
|
||||
library.localUpdateTimestamp(newData.id);
|
||||
if (callback) callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
[itemID, library, setSchema]
|
||||
);
|
||||
|
||||
return (
|
||||
<OssContext.Provider
|
||||
value={{
|
||||
|
@ -267,7 +325,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
|
|||
setOwner,
|
||||
setEditors,
|
||||
setAccessPolicy,
|
||||
setLocation
|
||||
setLocation,
|
||||
|
||||
savePositions,
|
||||
createOperation,
|
||||
deleteOperation
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -24,14 +24,15 @@ import {
|
|||
patchSubstituteConstituents,
|
||||
patchUploadTRS,
|
||||
patchVersion,
|
||||
postCreateConstituenta,
|
||||
postCreateVersion,
|
||||
postNewConstituenta,
|
||||
postSubscribe
|
||||
} from '@/app/backendAPI';
|
||||
import { type ErrorData } from '@/components/info/InfoError';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
|
||||
import { ILibraryUpdateData } from '@/models/library';
|
||||
import { ICstSubstituteData } from '@/models/oss';
|
||||
import {
|
||||
ConstituentaID,
|
||||
IConstituentaList,
|
||||
|
@ -39,7 +40,6 @@ import {
|
|||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IInlineSynthesisData,
|
||||
IRSForm,
|
||||
|
@ -399,7 +399,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
|
|||
const cstCreate = useCallback(
|
||||
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
|
||||
setProcessingError(undefined);
|
||||
postNewConstituenta(itemID, {
|
||||
postCreateConstituenta(itemID, {
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useLayoutEffect, 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 { useLibrary } from '@/context/LibraryContext';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { HelpTopic, Position2D } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { describeOperationType, labelOperationType } from '@/utils/labels';
|
||||
|
||||
import TabInputOperation from './TabInputOperation';
|
||||
import TabSynthesisOperation from './TabSynthesisOperation';
|
||||
|
||||
interface DlgCreateOperationProps {
|
||||
hideWindow: () => void;
|
||||
oss: IOperationSchema;
|
||||
positions: IOperationPosition[];
|
||||
insertPosition: Position2D;
|
||||
onCreate: (data: IOperationCreateData) => void;
|
||||
}
|
||||
|
||||
export enum TabID {
|
||||
INPUT = 0,
|
||||
SYNTHESIS = 1
|
||||
}
|
||||
|
||||
function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCreate }: DlgCreateOperationProps) {
|
||||
const library = useLibrary();
|
||||
const [activeTab, setActiveTab] = useState(TabID.INPUT);
|
||||
|
||||
const [alias, setAlias] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [comment, setComment] = useState('');
|
||||
const [inputs, setInputs] = useState<OperationID[]>([]);
|
||||
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
|
||||
|
||||
const isValid = useMemo(() => alias !== '', [alias]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (attachedID) {
|
||||
const schema = library.items.find(value => value.id === attachedID);
|
||||
if (schema) {
|
||||
setAlias(schema.alias);
|
||||
setTitle(schema.title);
|
||||
setComment(schema.comment);
|
||||
}
|
||||
}
|
||||
}, [attachedID, library]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
const data: IOperationCreateData = {
|
||||
item_data: {
|
||||
position_x: insertPosition.x,
|
||||
position_y: insertPosition.y,
|
||||
alias: alias,
|
||||
title: title,
|
||||
comment: comment,
|
||||
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
|
||||
result: activeTab === TabID.INPUT ? attachedID ?? null : null
|
||||
},
|
||||
positions: positions,
|
||||
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined
|
||||
};
|
||||
onCreate(data);
|
||||
};
|
||||
|
||||
const inputPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<TabInputOperation
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
attachedID={attachedID}
|
||||
setAttachedID={setAttachedID}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[alias, comment, title, attachedID]
|
||||
);
|
||||
|
||||
const synthesisPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<TabSynthesisOperation
|
||||
oss={oss}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
comment={comment}
|
||||
setComment={setComment}
|
||||
title={title}
|
||||
setTitle={setTitle}
|
||||
setInputs={setInputs}
|
||||
/>
|
||||
</TabPanel>
|
||||
),
|
||||
[oss, alias, comment, title]
|
||||
);
|
||||
|
||||
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={describeOperationType(OperationType.INPUT)}
|
||||
label={labelOperationType(OperationType.INPUT)}
|
||||
/>
|
||||
<TabLabel
|
||||
title={describeOperationType(OperationType.SYNTHESIS)}
|
||||
label={labelOperationType(OperationType.SYNTHESIS)}
|
||||
/>
|
||||
</TabList>
|
||||
|
||||
{inputPanel}
|
||||
{synthesisPanel}
|
||||
</Tabs>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DlgCreateOperation;
|
|
@ -0,0 +1,65 @@
|
|||
import PickSchema from '@/components/select/PickSchema';
|
||||
import Label from '@/components/ui/Label';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { limits, patterns } from '@/utils/constants';
|
||||
|
||||
interface TabInputOperationProps {
|
||||
alias: string;
|
||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
attachedID: LibraryItemID | undefined;
|
||||
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
|
||||
}
|
||||
|
||||
function TabInputOperation({
|
||||
alias,
|
||||
setAlias,
|
||||
title,
|
||||
setTitle,
|
||||
comment,
|
||||
setComment,
|
||||
attachedID,
|
||||
setAttachedID
|
||||
}: TabInputOperationProps) {
|
||||
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>
|
||||
|
||||
<Label text='Загружаемая концептуальная схема' />
|
||||
<PickSchema value={attachedID} onSelectValue={setAttachedID} rows={8} />
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabInputOperation;
|
|
@ -0,0 +1,92 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import SelectOperation from '@/components/select/SelectOperation';
|
||||
import FlexColumn from '@/components/ui/FlexColumn';
|
||||
import Label from '@/components/ui/Label';
|
||||
import TextArea from '@/components/ui/TextArea';
|
||||
import TextInput from '@/components/ui/TextInput';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { IOperation, IOperationSchema, OperationID } from '@/models/oss';
|
||||
import { limits, patterns } from '@/utils/constants';
|
||||
|
||||
interface TabSynthesisOperationProps {
|
||||
oss: IOperationSchema;
|
||||
alias: string;
|
||||
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||
title: string;
|
||||
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||
comment: string;
|
||||
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
}
|
||||
|
||||
function TabSynthesisOperation({
|
||||
oss,
|
||||
alias,
|
||||
setAlias,
|
||||
title,
|
||||
setTitle,
|
||||
comment,
|
||||
setComment,
|
||||
setInputs
|
||||
}: TabSynthesisOperationProps) {
|
||||
const [left, setLeft] = useState<IOperation | undefined>(undefined);
|
||||
const [right, setRight] = useState<IOperation | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const inputs: OperationID[] = [];
|
||||
if (left) {
|
||||
inputs.push(left.id);
|
||||
}
|
||||
if (right) {
|
||||
inputs.push(right.id);
|
||||
}
|
||||
setInputs(inputs);
|
||||
}, [setInputs, left, right]);
|
||||
|
||||
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>
|
||||
|
||||
<div className='flex justify-between'>
|
||||
<FlexColumn>
|
||||
<Label text='Аргумент 1' />
|
||||
<SelectOperation items={oss.items} value={left} onSelectValue={setLeft} />
|
||||
</FlexColumn>
|
||||
<FlexColumn>
|
||||
<Label text='Аргумент 2' className='text-right' />
|
||||
<SelectOperation items={oss.items} value={right} onSelectValue={setRight} />
|
||||
</FlexColumn>
|
||||
</div>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabSynthesisOperation;
|
|
@ -0,0 +1 @@
|
|||
export { default } from './DlgCreateOperation';
|
|
@ -8,7 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
|
|||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import useRSFormDetails from '@/hooks/useRSFormDetails';
|
||||
import { LibraryItemID } from '@/models/library';
|
||||
import { IInlineSynthesisData, IRSForm, ISubstitution } from '@/models/rsform';
|
||||
import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
|
||||
import TabConstituents from './TabConstituents';
|
||||
import TabSchema from './TabSchema';
|
||||
|
@ -30,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
|
|||
|
||||
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
|
||||
const [selected, setSelected] = useState<LibraryItemID[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
|
||||
|
||||
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { ErrorData } from '@/components/info/InfoError';
|
||||
import DataLoader from '@/components/wrap/DataLoader';
|
||||
import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform';
|
||||
import { ConstituentaID, IRSForm, ISingleSubstitution } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
import PickSubstitutions from '../../components/select/PickSubstitutions';
|
||||
|
@ -15,8 +15,8 @@ interface TabSubstitutionsProps {
|
|||
loading?: boolean;
|
||||
error?: ErrorData;
|
||||
|
||||
substitutions: ISubstitution[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
|
||||
substitutions: ISingleSubstitution[];
|
||||
setSubstitutions: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
|
||||
}
|
||||
|
||||
function TabSubstitutions({
|
||||
|
|
|
@ -6,7 +6,8 @@ import { useMemo, useState } from 'react';
|
|||
import PickSubstitutions from '@/components/select/PickSubstitutions';
|
||||
import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { ICstSubstituteData, ISubstitution } from '@/models/rsform';
|
||||
import { ICstSubstituteData } from '@/models/oss';
|
||||
import { ISingleSubstitution } from '@/models/rsform';
|
||||
import { prefixes } from '@/utils/constants';
|
||||
|
||||
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
||||
|
@ -16,7 +17,7 @@ interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
|
|||
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
|
||||
const { schema } = useRSForm();
|
||||
|
||||
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
|
||||
const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
|
||||
|
||||
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);
|
||||
|
||||
|
|
|
@ -2,22 +2,40 @@
|
|||
* Module: OSS data loading and processing.
|
||||
*/
|
||||
|
||||
import { IOperationSchema, IOperationSchemaData } from './oss';
|
||||
import { Graph } from './Graph';
|
||||
import { IOperation, IOperationSchema, IOperationSchemaData, OperationID } from './oss';
|
||||
|
||||
/**
|
||||
* Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
|
||||
*
|
||||
*/
|
||||
export class OssLoader {
|
||||
private schema: IOperationSchemaData;
|
||||
private oss: IOperationSchemaData;
|
||||
private graph: Graph = new Graph();
|
||||
private operationByID: Map<OperationID, IOperation> = new Map();
|
||||
|
||||
constructor(input: IOperationSchemaData) {
|
||||
this.schema = input;
|
||||
this.oss = input;
|
||||
}
|
||||
|
||||
produceOSS(): IOperationSchema {
|
||||
const result = this.schema as IOperationSchema;
|
||||
result.producedData = [1, 2, 3]; // TODO: put data processing here
|
||||
const result = this.oss as IOperationSchema;
|
||||
this.prepareLookups();
|
||||
this.createGraph();
|
||||
|
||||
result.operationByID = this.operationByID;
|
||||
result.graph = this.graph;
|
||||
return result;
|
||||
}
|
||||
|
||||
private prepareLookups() {
|
||||
this.oss.items.forEach(operation => {
|
||||
this.operationByID.set(operation.id, operation);
|
||||
this.graph.addNode(operation.id);
|
||||
});
|
||||
}
|
||||
|
||||
private createGraph() {
|
||||
this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,3 +177,11 @@ export interface GraphFilterParams {
|
|||
allowConstant: boolean;
|
||||
allowTheorem: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents XY Position.
|
||||
*/
|
||||
export interface Position2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
|
|
@ -2,18 +2,125 @@
|
|||
* Module: Schema of Synthesis Operations.
|
||||
*/
|
||||
|
||||
import { ILibraryItemData } from './library';
|
||||
import { Graph } from './Graph';
|
||||
import { ILibraryItemData, LibraryItemID } from './library';
|
||||
import { ConstituentaID } from './rsform';
|
||||
|
||||
/**
|
||||
* Represents backend data for Schema of Synthesis Operations.
|
||||
* Represents {@link IOperation} identifier type.
|
||||
*/
|
||||
export type OperationID = number;
|
||||
|
||||
/**
|
||||
* Represents {@link IOperation} type.
|
||||
*/
|
||||
export enum OperationType {
|
||||
INPUT = 'input',
|
||||
SYNTHESIS = 'synthesis'
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Operation.
|
||||
*/
|
||||
export interface IOperation {
|
||||
id: OperationID;
|
||||
operation_type: OperationType;
|
||||
oss: LibraryItemID;
|
||||
|
||||
alias: string;
|
||||
title: string;
|
||||
comment: string;
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
|
||||
result: LibraryItemID | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents {@link IOperation} position.
|
||||
*/
|
||||
export interface IOperationPosition extends Pick<IOperation, 'id' | 'position_x' | 'position_y'> {}
|
||||
|
||||
/**
|
||||
* Represents all {@link IOperation} positions in {@link IOperationSchema}.
|
||||
*/
|
||||
export interface IPositionsData {
|
||||
positions: IOperationPosition[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents target {@link IOperation}.
|
||||
*/
|
||||
export interface ITargetOperation extends IPositionsData {
|
||||
target: OperationID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents {@link IOperation} data, used in creation process.
|
||||
*/
|
||||
export interface IOperationCreateData extends IPositionsData {
|
||||
item_data: Pick<
|
||||
IOperation,
|
||||
'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result'
|
||||
>;
|
||||
arguments: OperationID[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents {@link IOperation} Argument.
|
||||
*/
|
||||
export interface IArgument {
|
||||
operation: OperationID;
|
||||
argument: OperationID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data, used in merging single {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstSubstitute {
|
||||
original: ConstituentaID;
|
||||
substitution: ConstituentaID;
|
||||
transfer_term: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data, used in merging multiple {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstSubstituteData {
|
||||
substitutions: ICstSubstitute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents {@link ICstSubstitute} extended data.
|
||||
*/
|
||||
export interface ICstSubstituteEx extends ICstSubstitute {
|
||||
original_alias: string;
|
||||
original_term: string;
|
||||
substitution_alias: string;
|
||||
substitution_term: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents backend data for {@link IOperationSchema}.
|
||||
*/
|
||||
export interface IOperationSchemaData extends ILibraryItemData {
|
||||
additional_data?: number[];
|
||||
items: IOperation[];
|
||||
arguments: IArgument[];
|
||||
substitutions: ICstSubstituteEx[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents Schema of Synthesis Operations.
|
||||
* Represents OperationSchema.
|
||||
*/
|
||||
export interface IOperationSchema extends IOperationSchemaData {
|
||||
producedData: number[]; // TODO: modify this to store calculated state on load
|
||||
graph: Graph;
|
||||
operationByID: Map<OperationID, IOperation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data response when creating {@link IOperation}.
|
||||
*/
|
||||
export interface IOperationCreatedResponse {
|
||||
new_operation: IOperation;
|
||||
oss: IOperationSchemaData;
|
||||
}
|
||||
|
|
18
rsconcept/frontend/src/models/ossAPI.ts
Normal file
18
rsconcept/frontend/src/models/ossAPI.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Module: API for OperationSystem.
|
||||
*/
|
||||
|
||||
import { TextMatcher } from '@/utils/utils';
|
||||
|
||||
import { IOperation } from './oss';
|
||||
|
||||
/**
|
||||
* Checks if a given target {@link IOperation} matches the specified query using.
|
||||
*
|
||||
* @param target - The target object to be matched.
|
||||
* @param query - The query string used for matching.
|
||||
*/
|
||||
export function matchOperation(target: IOperation, query: string): boolean {
|
||||
const matcher = new TextMatcher(query);
|
||||
return matcher.test(target.alias) || matcher.test(target.title);
|
||||
}
|
|
@ -5,10 +5,11 @@
|
|||
import { Graph } from '@/models/Graph';
|
||||
|
||||
import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library';
|
||||
import { ICstSubstitute } from './oss';
|
||||
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
|
||||
|
||||
/**
|
||||
* Represents Constituenta type.
|
||||
* Represents {@link IConstituenta} type.
|
||||
*/
|
||||
export enum CstType {
|
||||
BASE = 'basic',
|
||||
|
@ -21,7 +22,7 @@ export enum CstType {
|
|||
THEOREM = 'theorem'
|
||||
}
|
||||
|
||||
// CstType constant for category dividers in TemplateSchemas. TODO: create separate structure for templates
|
||||
// CstType constant for category dividers in TemplateSchemas
|
||||
export const CATEGORY_CST_TYPE = CstType.THEOREM;
|
||||
|
||||
/**
|
||||
|
@ -30,7 +31,7 @@ export const CATEGORY_CST_TYPE = CstType.THEOREM;
|
|||
export type Position = number;
|
||||
|
||||
/**
|
||||
* Represents {@link Constituenta} identifier type.
|
||||
* Represents {@link IConstituenta} identifier type.
|
||||
*/
|
||||
export type ConstituentaID = number;
|
||||
|
||||
|
@ -124,7 +125,7 @@ export interface IConstituentaList {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents constituenta data, used in creation process.
|
||||
* Represents {@link IConstituenta} data, used in creation process.
|
||||
*/
|
||||
export interface ICstCreateData
|
||||
extends Pick<
|
||||
|
@ -135,7 +136,7 @@ export interface ICstCreateData
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents data, used in ordering constituents in a list.
|
||||
* Represents data, used in ordering a list of {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstMovetoData extends IConstituentaList {
|
||||
move_to: Position;
|
||||
|
@ -158,32 +159,6 @@ export interface ICstUpdateData
|
|||
*/
|
||||
export interface ICstRenameData extends ITargetCst, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {}
|
||||
|
||||
/**
|
||||
* Represents data, used in merging single {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstSubstitute {
|
||||
original: ConstituentaID;
|
||||
substitution: ConstituentaID;
|
||||
transfer_term: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data, used in merging multiple {@link IConstituenta}.
|
||||
*/
|
||||
export interface ICstSubstituteData {
|
||||
substitutions: ICstSubstitute[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents single substitution for synthesis table.
|
||||
*/
|
||||
export interface ISubstitution {
|
||||
leftCst: IConstituenta;
|
||||
rightCst: IConstituenta;
|
||||
deleteRight: boolean;
|
||||
takeLeftTerm: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data response when creating {@link IConstituenta}.
|
||||
*/
|
||||
|
@ -265,6 +240,16 @@ export interface IVersionCreatedResponse {
|
|||
schema: IRSFormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents single substitution for synthesis table.
|
||||
*/
|
||||
export interface ISingleSubstitution {
|
||||
leftCst: IConstituenta;
|
||||
rightCst: IConstituenta;
|
||||
deleteRight: boolean;
|
||||
takeLeftTerm: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents input data for inline synthesis.
|
||||
*/
|
||||
|
|
|
@ -2,19 +2,22 @@
|
|||
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { storage } from '@/utils/constants';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
import OssFlow from './OssFlow';
|
||||
|
||||
function EditorOssGraph() {
|
||||
const controller = useOssEdit();
|
||||
interface EditorOssGraphProps {
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
||||
const [showGrid, setShowGrid] = useLocalStorage<boolean>(storage.ossShowGrid, false);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<AnimateFade>
|
||||
<OssFlow controller={controller} />
|
||||
</AnimateFade>
|
||||
<OssFlow isModified={isModified} setIsModified={setIsModified} showGrid={showGrid} setShowGrid={setShowGrid} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,56 +1,45 @@
|
|||
import { CiSquareRemove } from 'react-icons/ci';
|
||||
import { PiPlugsConnected } from 'react-icons/pi';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { IconRSForm } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
interface InputNodeProps {
|
||||
id: string;
|
||||
data: {
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
|
||||
function InputNode({ id }: InputNodeProps) {
|
||||
function InputNode({ id, data }: InputNodeProps) {
|
||||
const controller = useOssEdit();
|
||||
console.log(controller.isMutable);
|
||||
const model = useOSS();
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('delete node ' + id);
|
||||
};
|
||||
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
|
||||
|
||||
const handleClick = () => {
|
||||
// controller.selectNode(id);
|
||||
// controller.showSelectInput();
|
||||
const handleOpenSchema = () => {
|
||||
controller.openOperationSchema(Number(id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type='target' position={Position.Bottom} />
|
||||
<div>
|
||||
<Handle type='source' position={Position.Bottom} />
|
||||
|
||||
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<CiSquareRemove className='icon-red' />}
|
||||
title='Удалить'
|
||||
onClick={handleDelete}
|
||||
color={'red'}
|
||||
/>
|
||||
<div>
|
||||
Тип: <strong>Ввод</strong>
|
||||
</div>
|
||||
<div>
|
||||
{/* Схема:{controller.getBind(id) === undefined ? '' : controller.getBind(id)} */}
|
||||
<strong>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<PiPlugsConnected className='icon-green' />}
|
||||
title='Привязать схему'
|
||||
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
|
||||
noHover
|
||||
title='Связанная КС'
|
||||
onClick={() => {
|
||||
handleClick();
|
||||
handleOpenSchema();
|
||||
}}
|
||||
disabled={!hasFile}
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
<div className='flex-grow text-center text-sm'>{data.label}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,68 +1,47 @@
|
|||
import { CiSquareRemove } from 'react-icons/ci';
|
||||
import { IoGitNetworkSharp } from 'react-icons/io5';
|
||||
import { VscDebugStart } from 'react-icons/vsc';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { IconRSForm } from '@/components/Icons';
|
||||
import MiniButton from '@/components/ui/MiniButton.tsx';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
interface OperationNodeProps {
|
||||
id: string;
|
||||
data: {
|
||||
label: string;
|
||||
};
|
||||
}
|
||||
|
||||
function OperationNode({ id }: OperationNodeProps) {
|
||||
function OperationNode({ id, data }: OperationNodeProps) {
|
||||
const controller = useOssEdit();
|
||||
console.log(controller.isMutable);
|
||||
const model = useOSS();
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('delete node ' + id);
|
||||
// onDelete(id);
|
||||
};
|
||||
const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
|
||||
|
||||
const handleEditOperation = () => {
|
||||
console.log('edit operation ' + id);
|
||||
//controller.selectNode(id);
|
||||
//controller.showSynthesis();
|
||||
};
|
||||
|
||||
const handleRunOperation = () => {
|
||||
console.log('run operation');
|
||||
// controller.singleSynthesis(id);
|
||||
const handleOpenSchema = () => {
|
||||
controller.openOperationSchema(Number(id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type='target' position={Position.Bottom} />
|
||||
<div>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<CiSquareRemove className='icon-red' />}
|
||||
title='Удалить'
|
||||
onClick={handleDelete}
|
||||
color={'red'}
|
||||
/>
|
||||
<div>
|
||||
Тип: <strong>Отождествление</strong>
|
||||
</div>
|
||||
<div>
|
||||
Схема: <strong></strong>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<VscDebugStart className='icon-green' />}
|
||||
title='Синтез'
|
||||
onClick={() => handleRunOperation()}
|
||||
/>
|
||||
<MiniButton
|
||||
className='float-right'
|
||||
icon={<IoGitNetworkSharp className='icon-green' />}
|
||||
title='Отождествления'
|
||||
onClick={() => handleEditOperation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Handle type='source' position={Position.Bottom} />
|
||||
|
||||
<Handle type='source' position={Position.Top} id='a' style={{ left: 50 }} />
|
||||
<Handle type='source' position={Position.Top} id='b' style={{ right: 50, left: 'auto' }} />
|
||||
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
|
||||
<MiniButton
|
||||
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
|
||||
noHover
|
||||
title='Связанная КС'
|
||||
onClick={() => {
|
||||
handleOpenSchema();
|
||||
}}
|
||||
disabled={!hasFile}
|
||||
/>
|
||||
</Overlay>
|
||||
<div className='flex-grow text-center text-sm'>{data.label}</div>
|
||||
|
||||
<Handle type='target' position={Position.Top} id='left' style={{ left: 40 }} />
|
||||
<Handle type='target' position={Position.Top} id='right' style={{ right: 40, left: 'auto' }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,49 +1,248 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { NodeTypes, ProOptions, ReactFlow } from 'reactflow';
|
||||
import { toSvg } from 'html-to-image';
|
||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Background,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeTypes,
|
||||
ProOptions,
|
||||
ReactFlow,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useOnSelectionChange,
|
||||
useReactFlow
|
||||
} from 'reactflow';
|
||||
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errors } from '@/utils/labels';
|
||||
|
||||
import { IOssEditContext } from '../OssEditContext';
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
import InputNode from './InputNode';
|
||||
import OperationNode from './OperationNode';
|
||||
|
||||
const OssNodeTypes: NodeTypes = {
|
||||
synthesis: OperationNode,
|
||||
input: InputNode
|
||||
};
|
||||
import ToolbarOssGraph from './ToolbarOssGraph';
|
||||
|
||||
interface OssFlowProps {
|
||||
controller: IOssEditContext;
|
||||
isModified: boolean;
|
||||
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showGrid: boolean;
|
||||
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
function OssFlow({ controller }: OssFlowProps) {
|
||||
const { calculateHeight } = useConceptOptions();
|
||||
function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) {
|
||||
const { calculateHeight, colors } = useConceptOptions();
|
||||
const model = useOSS();
|
||||
const controller = useOssEdit();
|
||||
const flow = useReactFlow();
|
||||
|
||||
console.log(model.loading);
|
||||
console.log(controller.isMutable);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [toggleReset, setToggleReset] = useState(false);
|
||||
|
||||
const initialNodes = [
|
||||
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'input' },
|
||||
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' }, type: 'synthesis' }
|
||||
];
|
||||
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
const onSelectionChange = useCallback(
|
||||
({ nodes }: { nodes: Node[] }) => {
|
||||
controller.setSelected(nodes.map(node => Number(node.id)));
|
||||
console.log(nodes);
|
||||
},
|
||||
[controller]
|
||||
);
|
||||
|
||||
const proOptions: ProOptions = { hideAttribution: true };
|
||||
useOnSelectionChange({
|
||||
onChange: onSelectionChange
|
||||
});
|
||||
|
||||
const canvasWidth = useMemo(() => {
|
||||
return 'calc(100vw - 1rem)';
|
||||
useLayoutEffect(() => {
|
||||
if (!model.schema) {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
} else {
|
||||
setNodes(
|
||||
model.schema.items.map(operation => ({
|
||||
id: String(operation.id),
|
||||
data: { label: operation.alias },
|
||||
position: { x: operation.position_x, y: operation.position_y },
|
||||
type: operation.operation_type.toString()
|
||||
}))
|
||||
);
|
||||
setEdges(
|
||||
model.schema.arguments.map((argument, index) => ({
|
||||
id: String(index),
|
||||
source: String(argument.argument),
|
||||
target: String(argument.operation),
|
||||
targetHandle:
|
||||
model.schema!.operationByID.get(argument.argument)!.position_x >
|
||||
model.schema!.operationByID.get(argument.operation)!.position_x
|
||||
? 'right'
|
||||
: 'left'
|
||||
}))
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setIsModified(false);
|
||||
}, PARAMETER.graphRefreshDelay);
|
||||
}, [model.schema, setNodes, setEdges, setIsModified, toggleReset]);
|
||||
|
||||
const getPositions = useCallback(
|
||||
() =>
|
||||
nodes.map(node => ({
|
||||
id: Number(node.id),
|
||||
position_x: node.position.x,
|
||||
position_y: node.position.y
|
||||
})),
|
||||
[nodes]
|
||||
);
|
||||
|
||||
const handleNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
if (changes.some(change => change.type === 'position' && change.position)) {
|
||||
setIsModified(true);
|
||||
}
|
||||
onNodesChange(changes);
|
||||
},
|
||||
[onNodesChange, setIsModified]
|
||||
);
|
||||
|
||||
const handleSavePositions = useCallback(() => {
|
||||
controller.savePositions(getPositions(), () => setIsModified(false));
|
||||
}, [controller, getPositions, setIsModified]);
|
||||
|
||||
const handleCreateOperation = useCallback(() => {
|
||||
const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
console.log(center);
|
||||
controller.promptCreateOperation(center.x, center.y, getPositions());
|
||||
}, [controller, getPositions, flow]);
|
||||
|
||||
const handleDeleteOperation = useCallback(() => {
|
||||
if (controller.selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
controller.deleteOperation(controller.selected[0], getPositions());
|
||||
}, [controller, getPositions]);
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, [flow]);
|
||||
|
||||
const handleResetPositions = useCallback(() => {
|
||||
setToggleReset(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleSaveImage = useCallback(() => {
|
||||
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
|
||||
if (canvas === null) {
|
||||
toast.error(errors.imageFailed);
|
||||
return;
|
||||
}
|
||||
|
||||
const imageWidth = PARAMETER.ossImageWidth;
|
||||
const imageHeight = PARAMETER.ossImageHeight;
|
||||
const nodesBounds = getNodesBounds(nodes);
|
||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, 0.5, 2);
|
||||
toSvg(canvas, {
|
||||
backgroundColor: colors.bgDefault,
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
style: {
|
||||
width: String(imageWidth),
|
||||
height: String(imageHeight),
|
||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`
|
||||
}
|
||||
})
|
||||
.then(dataURL => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('download', 'reactflow.svg');
|
||||
a.setAttribute('href', dataURL);
|
||||
a.click();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
toast.error(errors.imageFailed);
|
||||
});
|
||||
}, [colors, nodes]);
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
// Hotkeys implementation
|
||||
if (controller.isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (!controller.isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSavePositions();
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Delete') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleDeleteOperation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []);
|
||||
const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []);
|
||||
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
|
||||
|
||||
const OssNodeTypes: NodeTypes = useMemo(
|
||||
() => ({
|
||||
synthesis: OperationNode,
|
||||
input: InputNode
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const graph = useMemo(
|
||||
() => (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
fitView
|
||||
proOptions={proOptions}
|
||||
nodeTypes={OssNodeTypes}
|
||||
maxZoom={2}
|
||||
minZoom={0.75}
|
||||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[10, 10]}
|
||||
>
|
||||
{showGrid ? <Background gap={10} /> : null}
|
||||
</ReactFlow>
|
||||
),
|
||||
[nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes, showGrid]
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
||||
<ToolbarOssGraph
|
||||
isModified={isModified}
|
||||
showGrid={showGrid}
|
||||
onFitView={handleFitView}
|
||||
onCreate={handleCreateOperation}
|
||||
onDelete={handleDeleteOperation}
|
||||
onResetPositions={handleResetPositions}
|
||||
onSavePositions={handleSavePositions}
|
||||
onSaveImage={handleSaveImage}
|
||||
toggleShowGrid={() => setShowGrid(prev => !prev)}
|
||||
/>
|
||||
</Overlay>
|
||||
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
|
||||
<ReactFlow nodes={initialNodes} edges={initialEdges} fitView proOptions={proOptions} nodeTypes={OssNodeTypes} />
|
||||
{graph}
|
||||
</div>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { IconDestroy, IconFitImage, IconGrid, IconImage, IconNewItem, IconReset, IconSave } from '@/components/Icons';
|
||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/labels';
|
||||
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
interface ToolbarOssGraphProps {
|
||||
isModified: boolean;
|
||||
showGrid: boolean;
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onFitView: () => void;
|
||||
onSaveImage: () => void;
|
||||
onSavePositions: () => void;
|
||||
onResetPositions: () => void;
|
||||
toggleShowGrid: () => void;
|
||||
}
|
||||
|
||||
function ToolbarOssGraph({
|
||||
isModified,
|
||||
showGrid,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onFitView,
|
||||
onSaveImage,
|
||||
onSavePositions,
|
||||
onResetPositions,
|
||||
toggleShowGrid
|
||||
}: ToolbarOssGraphProps) {
|
||||
const controller = useOssEdit();
|
||||
|
||||
return (
|
||||
<div className='cc-icons'>
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
disabled={controller.isProcessing || !isModified}
|
||||
onClick={onSavePositions}
|
||||
/>
|
||||
) : null}
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
title='Сбросить изменения'
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
disabled={!isModified}
|
||||
onClick={onResetPositions}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сбросить вид'
|
||||
onClick={onFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||
icon={
|
||||
showGrid ? (
|
||||
<IconGrid size='1.25rem' className='icon-green' />
|
||||
) : (
|
||||
<IconGrid size='1.25rem' className='icon-primary' />
|
||||
)
|
||||
}
|
||||
onClick={toggleShowGrid}
|
||||
/>
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
title='Новая операция'
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
disabled={controller.isProcessing}
|
||||
onClick={onCreate}
|
||||
/>
|
||||
) : null}
|
||||
{controller.isMutable ? (
|
||||
<MiniButton
|
||||
title='Удалить выбранную'
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
) : null}
|
||||
<MiniButton
|
||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сохранить изображение'
|
||||
onClick={onSaveImage}
|
||||
/>
|
||||
<BadgeHelp
|
||||
topic={HelpTopic.UI_OSS_GRAPH}
|
||||
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||
offset={4}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolbarOssGraph;
|
|
@ -4,19 +4,24 @@ import { AnimatePresence } from 'framer-motion';
|
|||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls } from '@/app/urls';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
|
||||
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
|
||||
import DlgEditEditors from '@/dialogs/DlgEditEditors';
|
||||
import { AccessPolicy } from '@/models/library';
|
||||
import { IOperationSchema } from '@/models/oss';
|
||||
import { Position2D } from '@/models/miscellaneous';
|
||||
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID } from '@/models/oss';
|
||||
import { UserID, UserLevel } from '@/models/user';
|
||||
import { information } from '@/utils/labels';
|
||||
|
||||
export interface IOssEditContext {
|
||||
schema?: IOperationSchema;
|
||||
selected: OperationID[];
|
||||
|
||||
isMutable: boolean;
|
||||
isProcessing: boolean;
|
||||
|
@ -27,7 +32,15 @@ export interface IOssEditContext {
|
|||
promptLocation: () => void;
|
||||
toggleSubscribe: () => void;
|
||||
|
||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
|
||||
share: () => void;
|
||||
|
||||
openOperationSchema: (target: OperationID) => void;
|
||||
|
||||
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
||||
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
|
||||
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||
}
|
||||
|
||||
const OssEditContext = createContext<IOssEditContext | null>(null);
|
||||
|
@ -41,11 +54,13 @@ export const useOssEdit = () => {
|
|||
|
||||
interface OssEditStateProps {
|
||||
// isModified: boolean;
|
||||
selected: OperationID[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const OssEditState = ({ children }: OssEditStateProps) => {
|
||||
// const router = useConceptNavigation();
|
||||
export const OssEditState = ({ selected, setSelected, children }: OssEditStateProps) => {
|
||||
const router = useConceptNavigation();
|
||||
const { user } = useAuth();
|
||||
const { adminMode } = useConceptOptions();
|
||||
const { accessLevel, setAccessLevel } = useAccessMode();
|
||||
|
@ -59,6 +74,10 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
|||
const [showEditEditors, setShowEditEditors] = useState(false);
|
||||
const [showEditLocation, setShowEditLocation] = useState(false);
|
||||
|
||||
const [showCreateOperation, setShowCreateOperation] = useState(false);
|
||||
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
||||
const [positions, setPositions] = useState<IOperationPosition[]>([]);
|
||||
|
||||
useLayoutEffect(
|
||||
() =>
|
||||
setAccessLevel(prev => {
|
||||
|
@ -136,10 +155,62 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
|||
[model]
|
||||
);
|
||||
|
||||
const openOperationSchema = useCallback(
|
||||
(target: OperationID) => {
|
||||
const node = model.schema?.operationByID.get(target);
|
||||
if (!node || !node.result) {
|
||||
return;
|
||||
}
|
||||
router.push(urls.schema(node.result));
|
||||
},
|
||||
[router, model]
|
||||
);
|
||||
|
||||
const savePositions = useCallback(
|
||||
(positions: IOperationPosition[], callback?: () => void) => {
|
||||
model.savePositions({ positions: positions }, () => {
|
||||
positions.forEach(item => {
|
||||
const operation = model.schema?.operationByID.get(item.id);
|
||||
if (operation) {
|
||||
operation.position_x = item.position_x;
|
||||
operation.position_y = item.position_y;
|
||||
}
|
||||
});
|
||||
toast.success(information.changesSaved);
|
||||
if (callback) callback();
|
||||
});
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => {
|
||||
setInsertPosition({ x: x, y: y });
|
||||
setPositions(positions);
|
||||
setShowCreateOperation(true);
|
||||
}, []);
|
||||
|
||||
const handleCreateOperation = useCallback(
|
||||
(data: IOperationCreateData) => {
|
||||
model.createOperation(data, operation => toast.success(information.newOperation(operation.alias)));
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
const deleteOperation = useCallback(
|
||||
(target: OperationID, positions: IOperationPosition[]) => {
|
||||
model.deleteOperation({ target: target, positions: positions }, () =>
|
||||
toast.success(information.operationDestroyed)
|
||||
);
|
||||
},
|
||||
[model]
|
||||
);
|
||||
|
||||
return (
|
||||
<OssEditContext.Provider
|
||||
value={{
|
||||
schema: model.schema,
|
||||
selected,
|
||||
|
||||
isMutable,
|
||||
isProcessing: model.processing,
|
||||
|
||||
|
@ -149,7 +220,13 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
|||
promptEditors,
|
||||
promptLocation,
|
||||
|
||||
share
|
||||
share,
|
||||
setSelected,
|
||||
|
||||
openOperationSchema,
|
||||
savePositions,
|
||||
promptCreateOperation,
|
||||
deleteOperation
|
||||
}}
|
||||
>
|
||||
{model.schema ? (
|
||||
|
@ -168,6 +245,15 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
|
|||
onChangeLocation={handleSetLocation}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateOperation ? (
|
||||
<DlgCreateOperation
|
||||
hideWindow={() => setShowCreateOperation(false)}
|
||||
oss={model.schema}
|
||||
positions={positions}
|
||||
insertPosition={insertPosition}
|
||||
onCreate={handleCreateOperation}
|
||||
/>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useLibrary } from '@/context/LibraryContext';
|
|||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||
import { useOSS } from '@/context/OssContext';
|
||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||
import { OperationID } from '@/models/oss';
|
||||
import { information, prompts } from '@/utils/labels';
|
||||
|
||||
import EditorRSForm from './EditorOssCard';
|
||||
|
@ -39,6 +40,7 @@ function OssTabs() {
|
|||
const { destroyItem } = useLibrary();
|
||||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -112,14 +114,14 @@ function OssTabs() {
|
|||
const graphPanel = useMemo(
|
||||
() => (
|
||||
<TabPanel>
|
||||
<EditorTermGraph />
|
||||
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
|
||||
</TabPanel>
|
||||
),
|
||||
[]
|
||||
[isModified]
|
||||
);
|
||||
|
||||
return (
|
||||
<OssEditState>
|
||||
<OssEditState selected={selected} setSelected={setSelected}>
|
||||
{loading ? <Loader /> : null}
|
||||
{errorLoading ? <ProcessError error={errorLoading} /> : null}
|
||||
{schema && !loading ? (
|
||||
|
|
|
@ -311,7 +311,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
|||
showParamsDialog={() => setShowParamsDialog(true)}
|
||||
onCreate={handleCreateCst}
|
||||
onDelete={handleDeleteCst}
|
||||
onResetViewpoint={() => setToggleResetView(prev => !prev)}
|
||||
onFitView={() => setToggleResetView(prev => !prev)}
|
||||
onSaveImage={handleSaveImage}
|
||||
toggleOrbit={() => setOrbit(prev => !prev)}
|
||||
toggleFoldDerived={handleFoldDerived}
|
||||
|
|
|
@ -29,7 +29,7 @@ interface ToolbarTermGraphProps {
|
|||
showParamsDialog: () => void;
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onResetViewpoint: () => void;
|
||||
onFitView: () => void;
|
||||
onSaveImage: () => void;
|
||||
|
||||
toggleFoldDerived: () => void;
|
||||
|
@ -48,7 +48,7 @@ function ToolbarTermGraph({
|
|||
showParamsDialog,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onResetViewpoint,
|
||||
onFitView,
|
||||
onSaveImage
|
||||
}: ToolbarTermGraphProps) {
|
||||
const controller = useRSEdit();
|
||||
|
@ -63,7 +63,7 @@ function ToolbarTermGraph({
|
|||
<MiniButton
|
||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
title='Граф целиком'
|
||||
onClick={onResetViewpoint}
|
||||
onClick={onFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={!noText ? 'Скрыть текст' : 'Отобразить текст'}
|
||||
|
|
|
@ -25,6 +25,7 @@ import DlgRenameCst from '@/dialogs/DlgRenameCst';
|
|||
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
|
||||
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
|
||||
import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library';
|
||||
import { ICstSubstituteData } from '@/models/oss';
|
||||
import {
|
||||
ConstituentaID,
|
||||
CstType,
|
||||
|
@ -33,7 +34,6 @@ import {
|
|||
ICstCreateData,
|
||||
ICstMovetoData,
|
||||
ICstRenameData,
|
||||
ICstSubstituteData,
|
||||
ICstUpdateData,
|
||||
IInlineSynthesisData,
|
||||
IRSForm,
|
||||
|
|
|
@ -34,21 +34,63 @@
|
|||
}
|
||||
}
|
||||
|
||||
.Flow {
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
.react-flow__handle {
|
||||
cursor: default !important;
|
||||
|
||||
border-color: var(--cl-bg-40);
|
||||
background-color: var(--cl-bg-120);
|
||||
|
||||
.selected & {
|
||||
border-color: var(--cd-bg-40);
|
||||
}
|
||||
|
||||
.react-flow__node-input {
|
||||
border: 1px solid #555;
|
||||
padding: 10px;
|
||||
.dark & {
|
||||
border-color: var(--cd-bg-40);
|
||||
background-color: var(--cd-bg-120);
|
||||
|
||||
.selected & {
|
||||
border-color: var(--cl-bg-40);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__pane {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:is(.react-flow__node-input, .react-flow__node-synthesis) {
|
||||
cursor: pointer;
|
||||
|
||||
border: 1px solid;
|
||||
padding: 2px;
|
||||
width: 150px;
|
||||
|
||||
border-radius: 5px;
|
||||
background-color: var(--cl-bg-120);
|
||||
|
||||
color: var(--cl-fg-100);
|
||||
border-color: var(--cl-bg-40);
|
||||
background-color: var(--cl-bg-120);
|
||||
|
||||
&:hover:not(.selected) {
|
||||
box-shadow: 0 0 0 2px var(--cl-prim-bg-80) !important;
|
||||
}
|
||||
|
||||
.react-flow__node-synthesis {
|
||||
border: 1px solid #555;
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
border-radius: 5px;
|
||||
&.selected {
|
||||
border-color: var(--cd-bg-40);
|
||||
}
|
||||
|
||||
.dark & {
|
||||
color: var(--cd-fg-100);
|
||||
border-color: var(--cd-bg-40);
|
||||
background-color: var(--cd-bg-120);
|
||||
|
||||
&:hover:not(.selected) {
|
||||
box-shadow: 0 0 0 3px var(--cd-prim-bg-80) !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--cl-bg-40);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ export const PARAMETER = {
|
|||
refreshTimeout: 100, // milliseconds delay for post-refresh actions
|
||||
minimalTimeout: 10, // milliseconds delay for fast updates
|
||||
|
||||
zoomDuration: 500, // milliseconds animation duration
|
||||
ossImageWidth: 1280, // pixels - size of OSS image
|
||||
ossImageHeight: 960, // pixels - size of OSS image
|
||||
|
||||
graphHoverXLimit: 0.4, // ratio to clientWidth used to determine which side of screen popup should be
|
||||
graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
|
||||
graphPopupDelay: 500, // milliseconds delay for graph popup selections
|
||||
|
@ -108,6 +112,8 @@ export const storage = {
|
|||
rsgraphSizing: 'rsgraph.sizing',
|
||||
rsgraphFoldHidden: 'rsgraph.fold_hidden',
|
||||
|
||||
ossShowGrid: 'oss.show_grid',
|
||||
|
||||
cstFilterMatch: 'cst.filter.match',
|
||||
cstFilterGraph: 'cst.filter.graph'
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { GramData, Grammeme, ReferenceType } from '@/models/language';
|
|||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||
import { validateLocation } from '@/models/libraryAPI';
|
||||
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
|
||||
import { OperationType } from '@/models/oss';
|
||||
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
|
||||
import {
|
||||
IArgumentInfo,
|
||||
|
@ -889,6 +890,28 @@ export function describeLibraryItemType(itemType: LibraryItemType): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves label for {@link OperationType}.
|
||||
*/
|
||||
export function labelOperationType(itemType: OperationType): string {
|
||||
// prettier-ignore
|
||||
switch (itemType) {
|
||||
case OperationType.INPUT: return 'Загрузка';
|
||||
case OperationType.SYNTHESIS: return 'Синтез';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves description for {@link OperationType}.
|
||||
*/
|
||||
export function describeOperationType(itemType: OperationType): string {
|
||||
// prettier-ignore
|
||||
switch (itemType) {
|
||||
case OperationType.INPUT: return 'Загрузка концептуальной схемы в ОСС';
|
||||
case OperationType.SYNTHESIS: return 'Синтез концептуальных схем';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI info descriptors.
|
||||
*/
|
||||
|
@ -909,12 +932,14 @@ export const information = {
|
|||
|
||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||
newLibraryItem: 'Схема успешно создана',
|
||||
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
|
||||
newVersion: (version: string) => `Версия создана: ${version}`,
|
||||
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
|
||||
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
|
||||
renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`,
|
||||
|
||||
versionDestroyed: 'Версия удалена',
|
||||
itemDestroyed: 'Схема удалена',
|
||||
operationDestroyed: 'Операция удалена',
|
||||
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
|
||||
};
|
||||
|
||||
|
@ -923,7 +948,8 @@ export const information = {
|
|||
*/
|
||||
export const errors = {
|
||||
astFailed: 'Невозможно построить дерево разбора',
|
||||
passwordsMismatch: 'Пароли не совпадают'
|
||||
passwordsMismatch: 'Пароли не совпадают',
|
||||
imageFailed: 'Ошибка при создании изображения'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
16
scripts/dev/GraphDB.ps1
Normal file
16
scripts/dev/GraphDB.ps1
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Generate DOT file for DB structure
|
||||
$backend = Resolve-Path -Path "${PSScriptRoot}\..\..\rsconcept\backend"
|
||||
|
||||
function GenerateDOT() {
|
||||
Set-Location $backend
|
||||
|
||||
$python = "${backend}\venv\Scripts\python.exe"
|
||||
$djangoSrc = "${backend}\manage.py"
|
||||
|
||||
& $python $djangoSrc graph_models -o visualizeDB.dot
|
||||
|
||||
notepad.exe "${backend}\visualizeDB.dot"
|
||||
Start-Process "https://dreampuf.github.io/GraphvizOnline"
|
||||
}
|
||||
|
||||
GenerateDOT
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
||||
|
||||
function RunLinters() {
|
||||
function RunCoverage() {
|
||||
BackendCoverage
|
||||
}
|
||||
|
||||
|
@ -20,4 +20,4 @@ function BackendCoverage() {
|
|||
Start-Process "$backend\htmlcov\index.html"
|
||||
}
|
||||
|
||||
RunLinters
|
||||
RunCoverage
|
Loading…
Reference in New Issue
Block a user