Compare commits

...

9 Commits

Author SHA1 Message Date
Ivan
338ad2bb98 Improve OSS UI
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
2024-07-24 18:11:28 +03:00
Ivan
b1491ccd35 Add extensions recomendations 2024-07-24 11:28:54 +03:00
Ivan
8977c0fadc Improve OSS frontend 2024-07-23 23:03:58 +03:00
Ivan
f277ce288b Fix cst create 2024-07-23 11:54:12 +03:00
Ivan
c91ff51afa Rafactoring: improving backend 2024-07-22 21:20:17 +03:00
Ivan
fbaf17ea58 Fix frontend 2024-07-21 22:50:43 +03:00
Ivan
b9ab054a00 Fix oss backend 2024-07-21 21:55:48 +03:00
Ivan
7b39b76498 Implementing basic oss graph pt2 2024-07-21 15:17:36 +03:00
Ivan
286abaf476 Implementing basic oss graph pt1 2024-07-20 18:26:32 +03:00
80 changed files with 1892 additions and 512 deletions

View File

@ -47,6 +47,7 @@ cover/
# Django # Django
rsconcept/frontend/static rsconcept/frontend/static
rsconcept/frontend/media rsconcept/frontend/media
visualizeDB.dot
*.log *.log
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ cover/
*.log *.log
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
visualizeDB.dot
# React # React

25
.vscode/extensions.json vendored Normal file
View 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
View File

@ -5,6 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
// Run Frontend + Backend with current Database
"name": "Run", "name": "Run",
"type": "PowerShell", "type": "PowerShell",
"request": "launch", "request": "launch",
@ -12,6 +13,7 @@
"args": [] "args": []
}, },
{ {
// Run Linters
"name": "Lint", "name": "Lint",
"type": "PowerShell", "type": "PowerShell",
"request": "launch", "request": "launch",
@ -19,6 +21,7 @@
"args": [] "args": []
}, },
{ {
// Run Tests
"name": "Test", "name": "Test",
"type": "PowerShell", "type": "PowerShell",
"request": "launch", "request": "launch",
@ -26,6 +29,7 @@
"args": [] "args": []
}, },
{ {
// Run Tests for backend for current file in Debug mode
"name": "BE-DebugTestFile", "name": "BE-DebugTestFile",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
@ -35,6 +39,7 @@
"django": true "django": true
}, },
{ {
// Run Tests for frontned in Debug mode
"name": "FE-DebugTestAll", "name": "FE-DebugTestAll",
"type": "node", "type": "node",
"request": "launch", "request": "launch",
@ -55,6 +60,7 @@
} }
}, },
{ {
// Run Browser in Debug mode (Backend should be running)
"name": "FE-Debug", "name": "FE-Debug",
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
@ -62,6 +68,7 @@
"webRoot": "${workspaceFolder}/rsconcept/frontend" "webRoot": "${workspaceFolder}/rsconcept/frontend"
}, },
{ {
// Run Backend in Debug mode
"name": "BE-Debug", "name": "BE-Debug",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
@ -70,6 +77,7 @@
"django": true "django": true
}, },
{ {
// Run Backend test coverage
"name": "BE-Coverage", "name": "BE-Coverage",
"type": "PowerShell", "type": "PowerShell",
"request": "launch", "request": "launch",
@ -77,11 +85,20 @@
"args": [] "args": []
}, },
{ {
// Recreate database, fill with initial data and Run Backend + Frontend
"name": "Restart", "name": "Restart",
"type": "PowerShell", "type": "PowerShell",
"request": "launch", "request": "launch",
"script": "${workspaceFolder}/scripts/dev/RunServer.ps1", "script": "${workspaceFolder}/scripts/dev/RunServer.ps1",
"args": ["-freshStart"] "args": ["-freshStart"]
},
{
// Create DOT file for visualizing database
"name": "BE-GraphDB",
"type": "PowerShell",
"request": "launch",
"script": "${workspaceFolder}/scripts/dev/GraphDB.ps1",
"args": []
} }
] ]
} }

View File

@ -21,6 +21,8 @@ This readme file is used mostly to document project dependencies
## ✨ Frontend [Vite + React + Typescript] ## ✨ Frontend [Vite + React + Typescript]
- to regenerate parsers use 'npm run generate' script
<details> <details>
<summary>npm install</summary> <summary>npm install</summary>
<pre> <pre>
@ -36,10 +38,12 @@ This readme file is used mostly to document project dependencies
- react-error-boundary - react-error-boundary
- react-pdf - react-pdf
- react-tooltip - react-tooltip
- reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
- framer-motion - framer-motion
- reagraph - reagraph
- html-to-image
- @tanstack/react-table - @tanstack/react-table
- @uiw/react-codemirror - @uiw/react-codemirror
- @uiw/codemirror-themes - @uiw/codemirror-themes
@ -54,6 +58,7 @@ This readme file is used mostly to document project dependencies
- autoprefixer - autoprefixer
- eslint-plugin-simple-import-sort - eslint-plugin-simple-import-sort
- eslint-plugin-tsdoc - eslint-plugin-tsdoc
- vite
- jest - jest
- ts-jest - ts-jest
- @types/jest - @types/jest
@ -65,11 +70,13 @@ This readme file is used mostly to document project dependencies
<pre> <pre>
- ESLint - ESLint
- Colorize - Colorize
- Tailwind CSS IntelliSense
- Code Spell Checker (eng + rus) - Code Spell Checker (eng + rus)
- Backticks - Backticks
- Svg Preview - Svg Preview
- TODO Highlight v2 - TODO Highlight v2
- Prettier - Prettier
- PowerShell (for Windows dev env)
</pre> </pre>
</details> </details>
<details> <details>

View File

@ -7,8 +7,24 @@ from . import models
class OperationAdmin(admin.ModelAdmin): class OperationAdmin(admin.ModelAdmin):
''' Admin model: Operation. ''' ''' Admin model: Operation. '''
ordering = ['oss'] ordering = ['oss']
list_display = ['oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y'] list_display = ['id', 'oss', 'operation_type', 'result', 'alias', 'title', 'comment', 'position_x', 'position_y']
search_fields = ['operation_type', 'title', 'alias'] 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.Operation, OperationAdmin)
admin.site.register(models.Argument, ArgumentAdmin)
admin.site.register(models.SynthesisSubstitution, SynthesisSubstitutionAdmin)

View File

@ -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='Схема синтеза'),
),
]

View File

@ -24,4 +24,4 @@ class Argument(Model):
unique_together = [['operation', 'argument']] unique_together = [['operation', 'argument']]
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.argument.pk} -> {self.operation.pk}' return f'{self.argument} -> {self.operation}'

View File

@ -21,7 +21,7 @@ class Operation(Model):
''' Operational schema Unit.''' ''' Operational schema Unit.'''
oss: ForeignKey = ForeignKey( oss: ForeignKey = ForeignKey(
verbose_name='Схема синтеза', verbose_name='Схема синтеза',
to='rsform.LibraryItem', to='oss.OperationSchema',
on_delete=CASCADE, on_delete=CASCADE,
related_name='items' related_name='items'
) )

View File

@ -3,7 +3,7 @@ from typing import Optional
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import Manager, QuerySet
from apps.rsform.models import LibraryItem, LibraryItemType from apps.rsform.models import LibraryItem, LibraryItemType
from shared import messages as msg from shared import messages as msg
@ -13,30 +13,37 @@ from .Operation import Operation
from .SynthesisSubstitution import SynthesisSubstitution from .SynthesisSubstitution import SynthesisSubstitution
class OperationSchema: class OperationSchema(LibraryItem):
''' Operations schema API. ''' ''' Operations schema API. '''
def __init__(self, item: LibraryItem): class Meta:
if item.item_type != LibraryItemType.OPERATION_SCHEMA: ''' Model metadata. '''
raise ValueError(msg.libraryTypeUnexpected()) proxy = True
self.item = item
@staticmethod class InternalManager(Manager):
def create(**kwargs) -> 'OperationSchema': ''' Object manager. '''
item = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs)
return OperationSchema(item=item) 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]: def operations(self) -> QuerySet[Operation]:
''' Get QuerySet containing all operations of current OSS. ''' ''' 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]: def arguments(self) -> QuerySet[Argument]:
''' Operation arguments. ''' ''' Operation arguments. '''
return Argument.objects.filter(operation__oss=self.item) return Argument.objects.filter(operation__oss=self)
def substitutions(self) -> QuerySet[SynthesisSubstitution]: def substitutions(self) -> QuerySet[SynthesisSubstitution]:
''' Operation substitutions. ''' ''' Operation substitutions. '''
return SynthesisSubstitution.objects.filter(operation__oss=self.item) return SynthesisSubstitution.objects.filter(operation__oss=self)
def update_positions(self, data: list[dict]): def update_positions(self, data: list[dict]):
''' Update positions. ''' ''' Update positions. '''
@ -53,11 +60,8 @@ class OperationSchema:
''' Insert new operation. ''' ''' Insert new operation. '''
if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists(): if kwargs['alias'] != '' and self.operations().filter(alias=kwargs['alias']).exists():
raise ValidationError(msg.aliasTaken(kwargs['alias'])) raise ValidationError(msg.aliasTaken(kwargs['alias']))
result = Operation.objects.create( result = Operation.objects.create(oss=self, **kwargs)
oss=self.item, self.save()
**kwargs
)
self.item.save()
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -69,7 +73,7 @@ class OperationSchema:
# deal with attached schema # deal with attached schema
# trigger on_change effects # trigger on_change effects
self.item.save() self.save()
@transaction.atomic @transaction.atomic
def set_input(self, target: Operation, schema: Optional[LibraryItem]): def set_input(self, target: Operation, schema: Optional[LibraryItem]):
@ -87,7 +91,7 @@ class OperationSchema:
# trigger on_change effects # trigger on_change effects
self.item.save() self.save()
@transaction.atomic @transaction.atomic
def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]: 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(): if Argument.objects.filter(operation=operation, argument=argument).exists():
return None return None
result = Argument.objects.create(operation=operation, argument=argument) result = Argument.objects.create(operation=operation, argument=argument)
self.item.save() self.save()
return result return result
@transaction.atomic @transaction.atomic
@ -109,7 +113,7 @@ class OperationSchema:
# trigger on_change effects # trigger on_change effects
self.item.save() self.save()
@transaction.atomic @transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]): def set_substitutions(self, target: Operation, substitutes: list[dict]):
@ -125,4 +129,4 @@ class OperationSchema:
# trigger on_change effects # trigger on_change effects
self.item.save() self.save()

View File

@ -33,4 +33,4 @@ class SynthesisSubstitution(Model):
verbose_name_plural = 'Таблицы отождествлений' verbose_name_plural = 'Таблицы отождествлений'
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.original.pk} -> {self.substitution.pk}' return f'{self.original} -> {self.substitution}'

View File

@ -2,7 +2,7 @@
from apps.rsform.models import LibraryItem, LibraryItemType from apps.rsform.models import LibraryItem, LibraryItemType
from .api_OSS import OperationSchema
from .Argument import Argument from .Argument import Argument
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .SynthesisSubstitution import SynthesisSubstitution from .SynthesisSubstitution import SynthesisSubstitution

View File

@ -2,7 +2,7 @@
from apps.rsform.serializers import LibraryItemSerializer from apps.rsform.serializers import LibraryItemSerializer
from .basics import OperationPositionSerializer, PositionsSerializer from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,

View File

@ -14,3 +14,15 @@ class PositionsSerializer(serializers.Serializer):
positions = serializers.ListField( positions = serializers.ListField(
child=OperationPositionSerializer() 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()

View File

@ -10,7 +10,7 @@ from apps.rsform.serializers import LibraryItemDetailsSerializer
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType from ..models import Argument, Operation, OperationSchema, OperationType
from .basics import OperationPositionSerializer from .basics import OperationPositionSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer): class OperationSerializer(serializers.ModelSerializer):
@ -42,9 +42,10 @@ class OperationCreateSerializer(serializers.Serializer):
model = Operation model = Operation
fields = \ fields = \
'alias', 'operation_type', 'title', \ 'alias', 'operation_type', 'title', \
'comment', 'position_x', 'position_y' 'comment', 'result', 'position_x', 'position_y'
item_data = OperationData() item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
positions = serializers.ListField( positions = serializers.ListField(
child=OperationPositionSerializer(), child=OperationPositionSerializer(),
default=[] default=[]
@ -75,30 +76,32 @@ class OperationSchemaSerializer(serializers.ModelSerializer):
items = serializers.ListField( items = serializers.ListField(
child=OperationSerializer() child=OperationSerializer()
) )
graph = serializers.ListField( arguments = serializers.ListField(
child=ArgumentSerializer() child=ArgumentSerializer()
) )
substitutions = serializers.ListField(
child=SubstitutionExSerializer()
)
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = LibraryItem model = OperationSchema
fields = '__all__' fields = '__all__'
def to_representation(self, instance: LibraryItem): def to_representation(self, instance: OperationSchema):
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
oss = OperationSchema(instance)
result['items'] = [] result['items'] = []
for operation in oss.operations(): for operation in instance.operations():
result['items'].append(OperationSerializer(operation).data) result['items'].append(OperationSerializer(operation).data)
result['graph'] = [] result['arguments'] = []
for argument in oss.arguments(): for argument in instance.arguments():
result['graph'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)
result['substitutions'] = [] result['substitutions'] = []
for substitution in oss.substitutions().values( for substitution in instance.substitutions().values(
'operation', 'operation',
'original', 'original',
'transfer_term',
'substitution', 'substitution',
'transfer_term',
original_alias=F('original__alias'), original_alias=F('original__alias'),
original_term=F('original__term_resolved'), original_term=F('original__term_resolved'),
substitution_alias=F('substitution__alias'), substitution_alias=F('substitution__alias'),

View File

@ -1 +1,4 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Argument import *
from .t_Operation import *
from .t_SynthesisSubstitution import *

View 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)

View 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)

View File

@ -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)

View File

@ -12,29 +12,29 @@ class TestOssViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) self.owned = OperationSchema.objects.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.item.pk self.owned_id = self.owned.pk
self.unowned = OperationSchema.create(title='Test2', alias='T2') self.unowned = OperationSchema.objects.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.item.pk self.unowned_id = self.unowned.pk
self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) self.private = OperationSchema.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.item.pk self.private_id = self.private.pk
self.invalid_id = self.private.item.pk + 1337 self.invalid_id = self.private.pk + 1337
def populateData(self): 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.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.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
self.operation1 = self.owned.create_operation( self.operation1 = self.owned.create_operation(
alias='1', alias='1',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks1.item result=self.ks1
) )
self.operation2 = self.owned.create_operation( self.operation2 = self.owned.create_operation(
alias='2', alias='2',
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks2.item result=self.ks2
) )
self.operation3 = self.owned.create_operation( self.operation3 = self.owned.create_operation(
alias='3', alias='3',
@ -53,12 +53,12 @@ class TestOssViewset(EndpointTester):
self.populateData() self.populateData()
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk) self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title) self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.item.alias) self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.item.location) self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.visible) self.assertEqual(response.data['visible'], self.owned.visible)
self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) 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_alias'], self.ks2x1.alias)
self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved) self.assertEqual(sub['substitution_term'], self.ks2x1.term_resolved)
graph = response.data['graph'] arguments = response.data['arguments']
self.assertEqual(len(graph), 2) self.assertEqual(len(arguments), 2)
self.assertEqual(graph[0]['operation'], self.operation3.pk) self.assertEqual(arguments[0]['operation'], self.operation3.pk)
self.assertEqual(graph[0]['argument'], self.operation1.pk) self.assertEqual(arguments[0]['argument'], self.operation1.pk)
self.assertEqual(graph[1]['operation'], self.operation3.pk) self.assertEqual(arguments[1]['operation'], self.operation3.pk)
self.assertEqual(graph[1]['argument'], self.operation2.pk) self.assertEqual(arguments[1]['argument'], self.operation2.pk)
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_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['comment'], data['item_data']['comment'])
self.assertEqual(new_operation['position_x'], data['item_data']['position_x']) self.assertEqual(new_operation['position_x'], data['item_data']['position_x'])
self.assertEqual(new_operation['position_y'], data['item_data']['position_y']) self.assertEqual(new_operation['position_y'], data['item_data']['position_y'])
self.assertEqual(new_operation['result'], None)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x']) self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x'])
self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y']) self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y'])
@ -166,6 +167,42 @@ class TestOssViewset(EndpointTester):
self.toggle_admin(True) self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id) 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') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self): def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id) self.executeNotFound(item=self.invalid_id)

View File

@ -20,11 +20,11 @@ from .. import serializers as s
@extend_schema_view() @extend_schema_view()
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: OperationSchema. ''' ''' Endpoint: OperationSchema. '''
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.OPERATION_SCHEMA) queryset = m.OperationSchema.objects.all()
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.OperationSchema: 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): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
@ -52,7 +52,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['get'], url_path='details') @action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk): def details(self, request: Request, pk):
''' Endpoint: Detailed OSS data. ''' ''' Endpoint: Detailed OSS data. '''
serializer = s.OperationSchemaSerializer(cast(m.LibraryItem, self.get_object())) serializer = s.OperationSchemaSerializer(self._get_schema())
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=serializer.data data=serializer.data
@ -98,13 +98,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic(): with transaction.atomic():
schema.update_positions(serializer.validated_data['positions']) schema.update_positions(serializer.validated_data['positions'])
new_operation = schema.create_operation(**serializer.validated_data['item_data']) 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( response = Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': s.OperationSerializer(new_operation).data, 'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(schema.item).data 'oss': s.OperationSchemaSerializer(schema).data
} }
) )
return response return response
@ -126,16 +129,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
schema = self._get_schema() schema = self._get_schema()
serializer = s.OperationDeleteSerializer( serializer = s.OperationDeleteSerializer(
data=request.data, data=request.data,
context={'oss': schema.item} context={'oss': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
schema.update_positions(serializer.validated_data['positions']) schema.update_positions(serializer.validated_data['positions'])
schema.delete_operation(serializer.validated_data['target']) schema.delete_operation(serializer.validated_data['target'])
schema.item.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(schema.item).data data=s.OperationSchemaSerializer(schema).data
) )

View File

@ -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='Источник'),
),
]

View File

@ -40,7 +40,7 @@ class Constituenta(Model):
''' Constituenta is the base unit for every conceptual schema. ''' ''' Constituenta is the base unit for every conceptual schema. '''
schema: ForeignKey = ForeignKey( schema: ForeignKey = ForeignKey(
verbose_name='Концептуальная схема', verbose_name='Концептуальная схема',
to='rsform.LibraryItem', to='rsform.RSForm',
on_delete=CASCADE on_delete=CASCADE
) )
order: PositiveIntegerField = PositiveIntegerField( order: PositiveIntegerField = PositiveIntegerField(

View File

@ -6,7 +6,7 @@ class LibraryTemplate(Model):
''' Template for library items and constituents. ''' ''' Template for library items and constituents. '''
lib_source: ForeignKey = ForeignKey( lib_source: ForeignKey = ForeignKey(
verbose_name='Источник', verbose_name='Источник',
to='rsform.LibraryItem', to='rsform.RSForm',
on_delete=CASCADE, on_delete=CASCADE,
null=True null=True
) )

View File

@ -5,7 +5,7 @@ from typing import Optional, cast
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams from cctext import Entity, Resolver, TermForm, extract_entities, split_grams
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import Manager, QuerySet
from shared import messages as msg from shared import messages as msg
@ -28,21 +28,29 @@ from .Version import Version
_INSERT_LAST: int = -1 _INSERT_LAST: int = -1
class RSForm: class RSForm(LibraryItem):
''' RSForm is math form of conceptual schema. ''' ''' RSForm is math form of conceptual schema. '''
def __init__(self, item: LibraryItem): class Meta:
if item.item_type != LibraryItemType.RSFORM: ''' Model metadata. '''
raise ValueError(msg.libraryTypeUnexpected()) proxy = True
self.item = item
@staticmethod class InternalManager(Manager):
def create(**kwargs) -> 'RSForm': ''' Object manager. '''
return RSForm(LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs))
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]: def constituents(self) -> QuerySet[Constituenta]:
''' Get QuerySet containing all constituents of current RSForm. ''' ''' 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: def resolver(self) -> Resolver:
''' Create resolver for text references based on schema terms. ''' ''' Create resolver for text references based on schema terms. '''
@ -98,7 +106,7 @@ class RSForm:
''' Get maximum alias index for specific CstType. ''' ''' Get maximum alias index for specific CstType. '''
result: int = 0 result: int = 0
items = Constituenta.objects \ items = Constituenta.objects \
.filter(schema=self.item, cst_type=cst_type) \ .filter(schema=self, cst_type=cst_type) \
.order_by('-alias') \ .order_by('-alias') \
.values_list('alias', flat=True) .values_list('alias', flat=True)
for alias in items: for alias in items:
@ -150,13 +158,13 @@ class RSForm:
cst_type = guess_type(alias) cst_type = guess_type(alias)
self._shift_positions(position, 1) self._shift_positions(position, 1)
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self.item, schema=self,
order=position, order=position,
alias=alias, alias=alias,
cst_type=cst_type, cst_type=cst_type,
**kwargs **kwargs
) )
self.item.save() self.save()
result.refresh_from_db() result.refresh_from_db()
return result return result
@ -183,13 +191,13 @@ class RSForm:
result = deepcopy(items) result = deepcopy(items)
for cst in result: for cst in result:
cst.pk = None cst.pk = None
cst.schema = self.item cst.schema = self
cst.order = position cst.order = position
cst.alias = mapping[cst.alias] cst.alias = mapping[cst.alias]
cst.apply_mapping(mapping) cst.apply_mapping(mapping)
cst.save() cst.save()
position = position + 1 position = position + 1
self.item.save() self.save()
return result return result
@transaction.atomic @transaction.atomic
@ -213,7 +221,7 @@ class RSForm:
count_moved += 1 count_moved += 1
update_list.append(cst) update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])
self.item.save() self.save()
@transaction.atomic @transaction.atomic
def delete_cst(self, listCst): def delete_cst(self, listCst):
@ -222,7 +230,7 @@ class RSForm:
cst.delete() cst.delete()
self._reset_order() self._reset_order()
self.resolve_all_text() self.resolve_all_text()
self.item.save() self.save()
@transaction.atomic @transaction.atomic
def substitute( def substitute(
@ -296,7 +304,7 @@ class RSForm:
def create_version(self, version: str, description: str, data) -> Version: def create_version(self, version: str, description: str, data) -> Version:
''' Creates version for current state. ''' ''' Creates version for current state. '''
return Version.objects.create( return Version.objects.create(
item=self.item, item=self,
version=version, version=version,
description=description, description=description,
data=data data=data
@ -322,7 +330,7 @@ class RSForm:
prefix = get_type_prefix(cst_type) prefix = get_type_prefix(cst_type)
for text in expressions: for text in expressions:
new_item = Constituenta.objects.create( new_item = Constituenta.objects.create(
schema=self.item, schema=self,
order=position, order=position,
alias=f'{prefix}{free_index}', alias=f'{prefix}{free_index}',
definition_formal=text, definition_formal=text,
@ -332,7 +340,7 @@ class RSForm:
free_index = free_index + 1 free_index = free_index + 1
position = position + 1 position = position + 1
self.item.save() self.save()
return result return result
def _shift_positions(self, start: int, shift: int): def _shift_positions(self, start: int, shift: int):
@ -341,7 +349,7 @@ class RSForm:
update_list = \ update_list = \
Constituenta.objects \ Constituenta.objects \
.only('id', 'order', 'schema') \ .only('id', 'order', 'schema') \
.filter(schema=self.item, order__gte=start) .filter(schema=self.pk, order__gte=start)
for cst in update_list: for cst in update_list:
cst.order += shift cst.order += shift
Constituenta.objects.bulk_update(update_list, ['order']) Constituenta.objects.bulk_update(update_list, ['order'])

View File

@ -1,6 +1,6 @@
''' Django: Models. ''' ''' Django: Models. '''
from .api_RSForm import RSForm from .RSForm import RSForm
from .Constituenta import Constituenta, CstType, _empty_forms from .Constituenta import Constituenta, CstType, _empty_forms
from .Editor import Editor from .Editor import Editor
from .LibraryItem import ( from .LibraryItem import (

View File

@ -109,22 +109,21 @@ class CstSerializer(serializers.ModelSerializer):
def update(self, instance: Constituenta, validated_data) -> Constituenta: def update(self, instance: Constituenta, validated_data) -> Constituenta:
data = validated_data # Note: use alias for better code readability data = validated_data # Note: use alias for better code readability
schema = RSForm(instance.schema)
definition: Optional[str] = data['definition_raw'] if 'definition_raw' in data else None 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: Optional[str] = data['term_raw'] if 'term_raw' in data else None
term_changed = 'term_forms' in data term_changed = 'term_forms' in data
if definition is not None and definition != instance.definition_raw: 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: 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: if data['term_resolved'] != instance.term_resolved and 'term_forms' not in data:
data['term_forms'] = [] data['term_forms'] = []
term_changed = data['term_resolved'] != instance.term_resolved term_changed = data['term_resolved'] != instance.term_resolved
result: Constituenta = super().update(instance, data) result: Constituenta = super().update(instance, data)
if term_changed: if term_changed:
schema.on_term_change([result.id]) instance.schema.on_term_change([result.id])
result.refresh_from_db() result.refresh_from_db()
schema.item.save() instance.schema.save()
return result return result
@ -170,17 +169,16 @@ class RSFormSerializer(serializers.ModelSerializer):
model = LibraryItem model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: LibraryItem) -> dict: def to_representation(self, instance: RSForm) -> dict:
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
schema = RSForm(instance)
result['items'] = [] result['items'] = []
for cst in schema.constituents().order_by('order'): for cst in instance.constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data) result['items'].append(CstSerializer(cst).data)
return result return result
def to_versioned_data(self) -> dict: def to_versioned_data(self) -> dict:
''' Create serializable version representation without redundant data. ''' ''' 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['versions']
del result['subscribers'] del result['subscribers']
del result['editors'] del result['editors']
@ -197,14 +195,14 @@ class RSFormSerializer(serializers.ModelSerializer):
def from_versioned_data(self, version: int, data: dict) -> dict: def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version. ''' ''' Load data from version. '''
result = self.to_representation(cast(LibraryItem, self.instance)) result = self.to_representation(cast(RSForm, self.instance))
result['version'] = version result['version'] = version
return result | data return result | data
@transaction.atomic @transaction.atomic
def restore_from_version(self, data: dict): def restore_from_version(self, data: dict):
''' Load data from version. ''' ''' Load data from version. '''
schema = RSForm(cast(LibraryItem, self.instance)) schema = cast(RSForm, self.instance)
items: list[dict] = data['items'] items: list[dict] = data['items']
ids: list[int] = [item['id'] for item in items] ids: list[int] = [item['id'] for item in items]
processed: list[int] = [] processed: list[int] = []
@ -258,13 +256,13 @@ class RSFormParseSerializer(serializers.ModelSerializer):
model = LibraryItem model = LibraryItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance: LibraryItem): def to_representation(self, instance: RSForm):
result = RSFormSerializer(instance).data result = RSFormSerializer(instance).data
return self._parse_data(result) return self._parse_data(result)
def from_versioned_data(self, version: int, data: dict) -> dict: def from_versioned_data(self, version: int, data: dict) -> dict:
''' Load data from version and parse. ''' ''' Load data from version and parse. '''
item = cast(LibraryItem, self.instance) item = cast(RSForm, self.instance)
result = RSFormSerializer(item).from_versioned_data(version, data) result = RSFormSerializer(item).from_versioned_data(version, data)
return self._parse_data(result) return self._parse_data(result)
@ -283,7 +281,7 @@ class CstTargetSerializer(serializers.Serializer):
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.all())
def validate(self, attrs): def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema']) schema = cast(RSForm, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if schema and cst.schema != schema: if schema and cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -315,7 +313,7 @@ class CstRenameSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
attrs = super().validate(attrs) attrs = super().validate(attrs)
schema = cast(LibraryItem, self.context['schema']) schema = cast(RSForm, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if cst.schema != schema: if cst.schema != schema:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -326,7 +324,7 @@ class CstRenameSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.renameTrivial(new_alias) '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({ raise serializers.ValidationError({
'alias': msg.aliasTaken(new_alias) 'alias': msg.aliasTaken(new_alias)
}) })
@ -338,7 +336,7 @@ class CstListSerializer(serializers.Serializer):
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
def validate(self, attrs): def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema']) schema = cast(RSForm, self.context['schema'])
if not schema: if not schema:
return attrs return attrs
@ -370,7 +368,7 @@ class CstSubstituteSerializer(serializers.Serializer):
) )
def validate(self, attrs): def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema']) schema = cast(RSForm, self.context['schema'])
deleted = set() deleted = set()
for item in attrs['substitutions']: for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original']) original_cst = cast(Constituenta, item['original'])
@ -397,8 +395,8 @@ class CstSubstituteSerializer(serializers.Serializer):
class InlineSynthesisSerializer(serializers.Serializer): class InlineSynthesisSerializer(serializers.Serializer):
''' Serializer: Inline synthesis operation input. ''' ''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=LibraryItem.objects.all()) receiver = PKField(many=False, queryset=RSForm.objects.all())
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore source = PKField(many=False, queryset=RSForm.objects.all()) # type: ignore
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=CstSubstituteSerializerBase() child=CstSubstituteSerializerBase()
@ -406,8 +404,8 @@ class InlineSynthesisSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
user = cast(User, self.context['user']) user = cast(User, self.context['user'])
schema_in = cast(LibraryItem, attrs['source']) schema_in = cast(RSForm, attrs['source'])
schema_out = cast(LibraryItem, attrs['receiver']) schema_out = cast(RSForm, attrs['receiver'])
if user.is_anonymous or (schema_out.owner != user and not user.is_staff): if user.is_anonymous or (schema_out.owner != user and not user.is_staff):
raise PermissionDenied({ raise PermissionDenied({
'message': msg.schemaNotOwned(), 'message': msg.schemaNotOwned(),

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from shared import messages as msg from shared import messages as msg
from ..models import Constituenta, LibraryItem, RSForm from ..models import Constituenta, RSForm
from ..utils import fix_old_references from ..utils import fix_old_references
_CST_TYPE = 'constituenta' _CST_TYPE = 'constituenta'
@ -39,9 +39,9 @@ class RSFormTRSSerializer(serializers.Serializer):
def _prepare_json_rsform(schema: RSForm) -> dict: def _prepare_json_rsform(schema: RSForm) -> dict:
return { return {
'type': _TRS_TYPE, 'type': _TRS_TYPE,
'title': schema.item.title, 'title': schema.title,
'alias': schema.item.alias, 'alias': schema.alias,
'comment': schema.item.comment, 'comment': schema.comment,
'items': [], 'items': [],
'claimed': False, 'claimed': False,
'selection': [], 'selection': [],
@ -125,7 +125,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['comment'] = data.get('comment', '') result['comment'] = data.get('comment', '')
if 'id' in data: if 'id' in data:
result['id'] = data['id'] result['id'] = data['id']
self.instance = RSForm(LibraryItem.objects.get(pk=result['id'])) self.instance = RSForm.objects.get(pk=result['id'])
return result return result
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -139,7 +139,7 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def create(self, validated_data: dict) -> RSForm: def create(self, validated_data: dict) -> RSForm:
self.instance: RSForm = RSForm.create( self.instance: RSForm = RSForm.objects.create(
owner=validated_data.get('owner', None), owner=validated_data.get('owner', None),
alias=validated_data['alias'], alias=validated_data['alias'],
title=validated_data['title'], title=validated_data['title'],
@ -149,12 +149,12 @@ class RSFormTRSSerializer(serializers.Serializer):
access_policy=validated_data['access_policy'], access_policy=validated_data['access_policy'],
location=validated_data['location'] location=validated_data['location']
) )
self.instance.item.save() self.instance.save()
order = 1 order = 1
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=self.instance.item, schema=self.instance,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -167,11 +167,11 @@ class RSFormTRSSerializer(serializers.Serializer):
@transaction.atomic @transaction.atomic
def update(self, instance: RSForm, validated_data) -> RSForm: def update(self, instance: RSForm, validated_data) -> RSForm:
if 'alias' in validated_data: if 'alias' in validated_data:
instance.item.alias = validated_data['alias'] instance.alias = validated_data['alias']
if 'title' in validated_data: if 'title' in validated_data:
instance.item.title = validated_data['title'] instance.title = validated_data['title']
if 'comment' in validated_data: if 'comment' in validated_data:
instance.item.comment = validated_data['comment'] instance.comment = validated_data['comment']
order = 1 order = 1
prev_constituents = instance.constituents() prev_constituents = instance.constituents()
@ -188,7 +188,7 @@ class RSFormTRSSerializer(serializers.Serializer):
else: else:
cst = Constituenta( cst = Constituenta(
alias=cst_data['alias'], alias=cst_data['alias'],
schema=instance.item, schema=instance,
order=order, order=order,
cst_type=cst_data['cstType'], cst_type=cst_data['cstType'],
) )
@ -202,7 +202,7 @@ class RSFormTRSSerializer(serializers.Serializer):
prev_cst.delete() prev_cst.delete()
instance.resolve_all_text() instance.resolve_all_text()
instance.item.save() instance.save()
return instance return instance
@staticmethod @staticmethod

View File

@ -3,15 +3,15 @@ from django.db.utils import IntegrityError
from django.forms import ValidationError from django.forms import ValidationError
from django.test import TestCase 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): class TestConstituenta(TestCase):
''' Testing Constituenta model. ''' ''' Testing Constituenta model. '''
def setUp(self): def setUp(self):
self.schema1 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test1') self.schema1 = RSForm.objects.create(title='Test1')
self.schema2 = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test2') self.schema2 = RSForm.objects.create(title='Test2')
def test_str(self): def test_str(self):

View File

@ -1,7 +1,7 @@
''' Testing models: Editor. ''' ''' Testing models: Editor. '''
from django.test import TestCase 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): class TestEditor(TestCase):
@ -10,8 +10,7 @@ class TestEditor(TestCase):
def setUp(self): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
self.item = LibraryItem.objects.create( self.item = RSForm.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test', title='Test',
alias='КС1', alias='КС1',
owner=self.user1 owner=self.user1

View File

@ -11,49 +11,49 @@ class TestRSForm(TestCase):
def setUp(self): def setUp(self):
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') 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) self.assertNotEqual(self.user1, self.user2)
def test_constituents(self): def test_constituents(self):
schema1 = RSForm.create(title='Test1') schema1 = RSForm.objects.create(title='Test1')
schema2 = RSForm.create(title='Test2') schema2 = RSForm.objects.create(title='Test2')
self.assertFalse(schema1.constituents().exists()) self.assertFalse(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) Constituenta.objects.create(alias='X1', schema=schema1, order=1)
Constituenta.objects.create(alias='X2', schema=schema1.item, order=2) Constituenta.objects.create(alias='X2', schema=schema1, order=2)
self.assertTrue(schema1.constituents().exists()) self.assertTrue(schema1.constituents().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituents().exists())
self.assertEqual(schema1.constituents().count(), 2) self.assertEqual(schema1.constituents().count(), 2)
def test_get_max_index(self): def test_get_max_index(self):
schema1 = RSForm.create(title='Test1') schema1 = RSForm.objects.create(title='Test1')
Constituenta.objects.create(alias='X1', schema=schema1.item, order=1) Constituenta.objects.create(alias='X1', schema=schema1, order=1)
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.item, order=2) 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.BASE), 1)
self.assertEqual(schema1.get_max_index(CstType.TERM), 2) self.assertEqual(schema1.get_max_index(CstType.TERM), 2)
self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0) self.assertEqual(schema1.get_max_index(CstType.AXIOM), 0)
def test_insert_at(self): def test_insert_at(self):
schema = RSForm.create(title='Test') schema = RSForm.objects.create(title='Test')
x1 = schema.insert_new('X1') x1 = schema.insert_new('X1')
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(x1.schema, schema.item) self.assertEqual(x1.schema, schema)
x2 = schema.insert_new('X2', position=1) x2 = schema.insert_new('X2', position=1)
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x2.schema, schema.item) self.assertEqual(x2.schema, schema)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
x3 = schema.insert_new('X3', position=4) x3 = schema.insert_new('X3', position=4)
x2.refresh_from_db() x2.refresh_from_db()
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
self.assertEqual(x3.schema, schema.item) self.assertEqual(x3.schema, schema)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
@ -62,7 +62,7 @@ class TestRSForm(TestCase):
x2.refresh_from_db() x2.refresh_from_db()
x1.refresh_from_db() x1.refresh_from_db()
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
self.assertEqual(x4.schema, schema.item) self.assertEqual(x4.schema, schema)
self.assertEqual(x3.order, 4) self.assertEqual(x3.order, 4)
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x1.order, 2) self.assertEqual(x1.order, 2)
@ -94,11 +94,11 @@ class TestRSForm(TestCase):
def test_insert_last(self): def test_insert_last(self):
x1 = self.schema.insert_new('X1') x1 = self.schema.insert_new('X1')
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(x1.schema, self.schema.item) self.assertEqual(x1.schema, self.schema)
x2 = self.schema.insert_new('X2') x2 = self.schema.insert_new('X2')
self.assertEqual(x2.order, 2) self.assertEqual(x2.order, 2)
self.assertEqual(x2.schema, self.schema.item) self.assertEqual(x2.schema, self.schema)
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
def test_create_cst(self): def test_create_cst(self):

View File

@ -8,12 +8,12 @@ class TestConstituentaAPI(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.rsform_owned = RSForm.create(title='Test', alias='T1', owner=self.user) self.rsform_owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.rsform_unowned = RSForm.create(title='Test2', alias='T2') self.rsform_unowned = RSForm.objects.create(title='Test2', alias='T2')
self.cst1 = Constituenta.objects.create( self.cst1 = Constituenta.objects.create(
alias='X1', alias='X1',
cst_type=CstType.BASE, cst_type=CstType.BASE,
schema=self.rsform_owned.item, schema=self.rsform_owned,
order=1, order=1,
convention='Test', convention='Test',
term_raw='Test1', term_raw='Test1',
@ -22,7 +22,7 @@ class TestConstituentaAPI(EndpointTester):
self.cst2 = Constituenta.objects.create( self.cst2 = Constituenta.objects.create(
alias='X2', alias='X2',
cst_type=CstType.BASE, cst_type=CstType.BASE,
schema=self.rsform_unowned.item, schema=self.rsform_unowned,
order=1, order=1,
convention='Test1', convention='Test1',
term_raw='Test2', term_raw='Test2',
@ -30,7 +30,7 @@ class TestConstituentaAPI(EndpointTester):
) )
self.cst3 = Constituenta.objects.create( self.cst3 = Constituenta.objects.create(
alias='X3', alias='X3',
schema=self.rsform_owned.item, schema=self.rsform_owned,
order=2, order=2,
term_raw='Test3', term_raw='Test3',
term_resolved='Test3', term_resolved='Test3',

View File

@ -20,20 +20,16 @@ class TestLibraryViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = LibraryItem.objects.create( self.owned = RSForm.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test', title='Test',
alias='T1', alias='T1',
owner=self.user owner=self.user
) )
self.schema = RSForm(self.owned) self.unowned = RSForm.objects.create(
self.unowned = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test2', title='Test2',
alias='T2' alias='T2'
) )
self.common = LibraryItem.objects.create( self.common = RSForm.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test3', title='Test3',
alias='T3', alias='T3',
location=LocationHead.COMMON location=LocationHead.COMMON
@ -363,12 +359,12 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/clone', method='post') @decl_endpoint('/api/library/{item}/clone', method='post')
def test_clone_rsform(self): def test_clone_rsform(self):
x12 = self.schema.insert_new( x12 = self.owned.insert_new(
alias='X12', alias='X12',
term_raw='человек', term_raw='человек',
term_resolved='человек' term_resolved='человек'
) )
d2 = self.schema.insert_new( d2 = self.owned.insert_new(
alias='D2', alias='D2',
term_raw='@{X12|plur}', term_raw='@{X12|plur}',
term_resolved='люди' term_resolved='люди'

View File

@ -10,16 +10,16 @@ class TestInlineSynthesis(EndpointTester):
@decl_endpoint('/api/operations/inline-synthesis', method='patch') @decl_endpoint('/api/operations/inline-synthesis', method='patch')
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.schema1 = RSForm.create(title='Test1', alias='T1', owner=self.user) self.schema1 = RSForm.objects.create(title='Test1', alias='T1', owner=self.user)
self.schema2 = RSForm.create(title='Test2', alias='T2', owner=self.user) self.schema2 = RSForm.objects.create(title='Test2', alias='T2', owner=self.user)
self.unowned = RSForm.create(title='Test3', alias='T3') self.unowned = RSForm.objects.create(title='Test3', alias='T3')
def test_inline_synthesis_inputs(self): def test_inline_synthesis_inputs(self):
invalid_id = 1338 invalid_id = 1338
data = { data = {
'receiver': self.unowned.item.pk, 'receiver': self.unowned.pk,
'source': self.schema1.item.pk, 'source': self.schema1.pk,
'items': [], 'items': [],
'substitutions': [] 'substitutions': []
} }
@ -28,11 +28,11 @@ class TestInlineSynthesis(EndpointTester):
data['receiver'] = invalid_id data['receiver'] = invalid_id
self.executeBadData(data=data) self.executeBadData(data=data)
data['receiver'] = self.schema1.item.pk data['receiver'] = self.schema1.pk
data['source'] = invalid_id data['source'] = invalid_id
self.executeBadData(data=data) self.executeBadData(data=data)
data['source'] = self.schema1.item.pk data['source'] = self.schema1.pk
self.executeOK(data=data) self.executeOK(data=data)
data['items'] = [invalid_id] 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 ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
data = { data = {
'receiver': self.schema1.item.pk, 'receiver': self.schema1.pk,
'source': self.schema2.item.pk, 'source': self.schema2.pk,
'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk], 'items': [ks2_x1.pk, ks2_x2.pk, ks2_s1.pk, ks2_d1.pk],
'substitutions': [ 'substitutions': [
{ {

View File

@ -24,12 +24,12 @@ class TestRSFormViewset(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user) self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.item.pk self.owned_id = self.owned.pk
self.unowned = RSForm.create(title='Test2', alias='T2') self.unowned = RSForm.objects.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.item.pk self.unowned_id = self.unowned.pk
self.private = RSForm.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) self.private = RSForm.objects.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE)
self.private_id = self.private.item.pk self.private_id = self.private.pk
@decl_endpoint('/api/rsforms/create-detailed', method='post') @decl_endpoint('/api/rsforms/create-detailed', method='post')
@ -63,19 +63,19 @@ class TestRSFormViewset(EndpointTester):
) )
response = self.executeOK() response = self.executeOK()
self.assertFalse(response_contains(response, non_schema)) self.assertFalse(response_contains(response, non_schema))
self.assertTrue(response_contains(response, self.unowned.item)) self.assertTrue(response_contains(response, self.unowned))
self.assertTrue(response_contains(response, self.owned.item)) self.assertTrue(response_contains(response, self.owned))
@decl_endpoint('/api/rsforms/{item}/contents', method='get') @decl_endpoint('/api/rsforms/{item}/contents', method='get')
def test_contents(self): def test_contents(self):
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk) self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title) self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.item.alias) self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.item.location) self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.visible) self.assertEqual(response.data['visible'], self.owned.visible)
@decl_endpoint('/api/rsforms/{item}/details', method='get') @decl_endpoint('/api/rsforms/{item}/details', method='get')
@ -92,12 +92,12 @@ class TestRSFormViewset(EndpointTester):
) )
response = self.executeOK(item=self.owned_id) response = self.executeOK(item=self.owned_id)
self.assertEqual(response.data['owner'], self.owned.item.owner.pk) self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['title'], self.owned.item.title) self.assertEqual(response.data['title'], self.owned.title)
self.assertEqual(response.data['alias'], self.owned.item.alias) self.assertEqual(response.data['alias'], self.owned.alias)
self.assertEqual(response.data['location'], self.owned.item.location) self.assertEqual(response.data['location'], self.owned.location)
self.assertEqual(response.data['access_policy'], self.owned.item.access_policy) self.assertEqual(response.data['access_policy'], self.owned.access_policy)
self.assertEqual(response.data['visible'], self.owned.item.visible) self.assertEqual(response.data['visible'], self.owned.visible)
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['id'], x1.pk) 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') @decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
def test_export_trs(self): def test_export_trs(self):
schema = RSForm.create(title='Test') schema = RSForm.objects.create(title='Test')
schema.insert_new('X1') 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') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
with ZipFile(stream, 'r') as zipped_file: 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_raw, data['term_raw'])
self.assertEqual(x4.term_forms, data['term_forms']) 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') @decl_endpoint('/api/rsforms/{item}/cst-rename', method='patch')
def test_rename_constituenta(self): def test_rename_constituenta(self):
@ -387,7 +396,7 @@ class TestRSFormViewset(EndpointTester):
data = {'items': [x1.pk]} data = {'items': [x1.pk]}
response = self.executeOK(data=data) response = self.executeOK(data=data)
x2.refresh_from_db() x2.refresh_from_db()
self.owned.item.refresh_from_db() self.owned.refresh_from_db()
self.assertEqual(len(response.data['items']), 1) self.assertEqual(len(response.data['items']), 1)
self.assertEqual(self.owned.constituents().count(), 1) self.assertEqual(self.owned.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2') self.assertEqual(x2.alias, 'X2')
@ -449,16 +458,16 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/load-trs', method='patch') @decl_endpoint('/api/rsforms/{item}/load-trs', method='patch')
def test_load_trs(self): def test_load_trs(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
self.owned.item.title = 'Test11' self.owned.title = 'Test11'
self.owned.item.save() self.owned.save()
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_new('X1')
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False} data = {'file': file, 'load_metadata': False}
response = self.client.patch(self.endpoint, data=data, format='multipart') 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(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(len(response.data['items']), 25)
self.assertEqual(self.owned.constituents().count(), 25) self.assertEqual(self.owned.constituents().count(), 25)
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())

View File

@ -15,10 +15,9 @@ class TestVersionViews(EndpointTester):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user).item self.owned = RSForm.objects.create(title='Test', alias='T1', owner=self.user)
self.schema = RSForm(self.owned) self.unowned = RSForm.objects.create(title='Test2', alias='T2')
self.unowned = RSForm.create(title='Test2', alias='T2').item self.x1 = self.owned.insert_new(
self.x1 = self.schema.insert_new(
alias='X1', alias='X1',
convention='testStart' convention='testStart'
) )
@ -135,14 +134,14 @@ class TestVersionViews(EndpointTester):
@decl_endpoint('/api/versions/{version}/restore', method='patch') @decl_endpoint('/api/versions/{version}/restore', method='patch')
def test_restore_version(self): def test_restore_version(self):
x1 = self.x1 x1 = self.x1
x2 = self.schema.insert_new('X2') x2 = self.owned.insert_new('X2')
d1 = self.schema.insert_new('D1', term_raw='TestTerm') d1 = self.owned.insert_new('D1', term_raw='TestTerm')
data = {'version': '1.0.0', 'description': 'test'} data = {'version': '1.0.0', 'description': 'test'}
version_id = self._create_version(data=data) version_id = self._create_version(data=data)
invalid_id = version_id + 1337 invalid_id = version_id + 1337
d1.delete() d1.delete()
x3 = self.schema.insert_new('X3') x3 = self.owned.insert_new('X3')
x1.order = x3.order x1.order = x3.order
x1.convention = 'Test2' x1.convention = 'Test2'
x1.term_raw = 'Test' x1.term_raw = 'Test'

View File

@ -85,7 +85,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer = s.LibraryItemCloneSerializer(data=request.data) serializer = s.LibraryItemCloneSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
item = self._get_item() 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.pk = None
clone.owner = self.request.user clone.owner = self.request.user
clone.title = serializer.validated_data['title'] clone.title = serializer.validated_data['title']
@ -98,18 +102,16 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
clone.save() clone.save()
if clone.item_type == m.LibraryItemType.RSFORM: need_filter = 'items' in request.data
need_filter = 'items' in request.data for cst in schema.constituents():
for cst in m.RSForm(item).constituents(): if not need_filter or cst.pk in request.data['items']:
if not need_filter or cst.pk in request.data['items']: cst.pk = None
cst.pk = None cst.schema = clone
cst.schema = clone cst.save()
cst.save() return Response(
return Response( status=c.HTTP_201_CREATED,
status=c.HTTP_201_CREATED, data=s.RSFormParseSerializer(clone).data
data=s.RSFormParseSerializer(clone).data )
)
return Response(status=c.HTTP_400_BAD_REQUEST)
@extend_schema( @extend_schema(
summary='subscribe to item', summary='subscribe to item',

View File

@ -27,7 +27,7 @@ def inline_synthesis(request: Request):
) )
serializer.is_valid(raise_exception=True) 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']) items = cast(list[m.Constituenta], serializer.validated_data['items'])
with transaction.atomic(): with transaction.atomic():
@ -46,5 +46,5 @@ def inline_synthesis(request: Request):
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )

View File

@ -25,11 +25,11 @@ from .. import utils
@extend_schema_view() @extend_schema_view()
class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
''' Endpoint: RSForm operations. ''' ''' Endpoint: RSForm operations. '''
queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM) queryset = m.RSForm.objects.all()
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def _get_schema(self) -> m.RSForm: 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): def get_permissions(self):
''' Determine permission class. ''' ''' Determine permission class. '''
@ -72,7 +72,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
if 'insert_after' in data: if 'insert_after' in data and data['insert_after'] is not None:
try: try:
insert_after = m.Constituenta.objects.get(pk=data['insert_after']) insert_after = m.Constituenta.objects.get(pk=data['insert_after'])
except m.LibraryItem.DoesNotExist: except m.LibraryItem.DoesNotExist:
@ -81,12 +81,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
insert_after = None insert_after = None
new_cst = schema.create_cst(data, insert_after) new_cst = schema.create_cst(data, insert_after)
schema.item.refresh_from_db() schema.refresh_from_db()
response = Response( response = Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_cst': s.CstSerializer(new_cst).data, 'new_cst': s.CstSerializer(new_cst).data,
'schema': s.RSFormParseSerializer(schema.item).data 'schema': s.RSFormParseSerializer(schema).data
} }
) )
response['Location'] = new_cst.get_absolute_url() 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. ''' ''' Produce a term for every element of the target constituenta typification. '''
schema = self._get_schema() 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) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) 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'] cst_parse = next(item for item in schema_details if item['id'] == cst.id)['parse']
if not cst_parse['typification']: if not cst_parse['typification']:
return Response( return Response(
@ -125,7 +125,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'cst_list': result, '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): def cst_rename(self, request: Request, pk):
''' Rename constituenta possibly changing type. ''' ''' Rename constituenta possibly changing type. '''
schema = self._get_schema() 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) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) cst = cast(m.Constituenta, serializer.validated_data['target'])
@ -156,14 +156,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
with transaction.atomic(): with transaction.atomic():
cst.save() cst.save()
schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False) schema.apply_mapping(mapping={old_alias: cst.alias}, change_aliases=False)
schema.item.refresh_from_db() schema.refresh_from_db()
cst.refresh_from_db() cst.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'new_cst': s.CstSerializer(cst).data, 'new_cst': s.CstSerializer(cst).data,
'schema': s.RSFormParseSerializer(schema.item).data 'schema': s.RSFormParseSerializer(schema).data
} }
) )
@ -184,7 +184,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstSubstituteSerializer( serializer = s.CstSubstituteSerializer(
data=request.data, data=request.data,
context={'schema': schema.item} context={'schema': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -193,10 +193,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
schema.substitute(original, replacement, substitution['transfer_term']) schema.substitute(original, replacement, substitution['transfer_term'])
schema.item.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )
@extend_schema( @extend_schema(
@ -216,14 +216,14 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstListSerializer( serializer = s.CstListSerializer(
data=request.data, data=request.data,
context={'schema': schema.item} context={'schema': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.delete_cst(serializer.validated_data['items']) schema.delete_cst(serializer.validated_data['items'])
schema.item.refresh_from_db() schema.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )
@extend_schema( @extend_schema(
@ -243,7 +243,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema() schema = self._get_schema()
serializer = s.CstMoveSerializer( serializer = s.CstMoveSerializer(
data=request.data, data=request.data,
context={'schema': schema.item} context={'schema': schema}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.move_cst( schema.move_cst(
@ -252,7 +252,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
) )
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )
@extend_schema( @extend_schema(
@ -272,7 +272,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.reset_aliases() schema.reset_aliases()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )
@extend_schema( @extend_schema(
@ -292,7 +292,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema.restore_order() schema.restore_order()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.item).data data=s.RSFormParseSerializer(schema).data
) )
@extend_schema( @extend_schema(
@ -314,7 +314,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
schema = self._get_schema() schema = self._get_schema()
load_metadata = input_serializer.validated_data['load_metadata'] load_metadata = input_serializer.validated_data['load_metadata']
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) 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( serializer = s.RSFormTRSSerializer(
data=data, data=data,
@ -324,7 +324,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
result = serializer.save() result = serializer.save()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(result.item).data data=s.RSFormParseSerializer(result).data
) )
@extend_schema( @extend_schema(
@ -357,7 +357,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['get'], url_path='details') @action(detail=True, methods=['get'], url_path='details')
def details(self, request: Request, pk): def details(self, request: Request, pk):
''' Endpoint: Detailed schema view including statuses and parse. ''' ''' 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( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=serializer.data data=serializer.data
@ -421,7 +421,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
''' Endpoint: Download Exteor compatible file. ''' ''' Endpoint: Download Exteor compatible file. '''
data = s.RSFormTRSSerializer(self._get_schema()).data data = s.RSFormTRSSerializer(self._get_schema()).data
file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) 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 = HttpResponse(file, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={filename}' response['Content-Disposition'] = f'attachment; filename={filename}'
return response return response
@ -451,7 +451,7 @@ class TrsImportView(views.APIView):
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = serializer.save() schema = serializer.save()
result = s.LibraryItemSerializer(schema.item) result = s.LibraryItemSerializer(schema)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=result.data data=result.data
@ -483,7 +483,7 @@ def create_rsform(request: Request):
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True}) serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer_rsform.is_valid(raise_exception=True) serializer_rsform.is_valid(raise_exception=True)
schema = serializer_rsform.save() schema = serializer_rsform.save()
result = s.LibraryItemSerializer(schema.item) result = s.LibraryItemSerializer(schema)
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=result.data data=result.data

View File

@ -42,10 +42,11 @@ class VersionViewset(
''' Restore version data into current item. ''' ''' Restore version data into current item. '''
version = cast(m.Version, self.get_object()) version = cast(m.Version, self.get_object())
item = cast(m.LibraryItem, version.item) 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( return Response(
status=c.HTTP_200_OK, 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): def create_version(request: Request, pk_item: int):
''' Endpoint: Create new version for RSForm copying current content. ''' ''' Endpoint: Create new version for RSForm copying current content. '''
try: try:
item = m.LibraryItem.objects.get(pk=pk_item) item = m.RSForm.objects.get(pk=pk_item)
except m.LibraryItem.DoesNotExist: except m.LibraryItem.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_404_NOT_FOUND)
creator = request.user creator = request.user
@ -75,7 +76,7 @@ def create_version(request: Request, pk_item: int):
version_input = s.VersionCreateSerializer(data=request.data) version_input = s.VersionCreateSerializer(data=request.data)
version_input.is_valid(raise_exception=True) version_input.is_valid(raise_exception=True)
data = s.RSFormSerializer(item).to_versioned_data() data = s.RSFormSerializer(item).to_versioned_data()
result = m.RSForm(item).create_version( result = item.create_version(
version=version_input.validated_data['version'], version=version_input.validated_data['version'],
description=version_input.validated_data['description'], description=version_input.validated_data['description'],
data=data 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): def retrieve_version(request: Request, pk_item: int, pk_version: int):
''' Endpoint: Retrieve version for RSForm. ''' ''' Endpoint: Retrieve version for RSForm. '''
try: try:
item = m.LibraryItem.objects.get(pk=pk_item) item = m.RSForm.objects.get(pk=pk_item)
except m.LibraryItem.DoesNotExist: except m.RSForm.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_404_NOT_FOUND)
try: try:
version = m.Version.objects.get(pk=pk_version) 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) version = m.Version.objects.get(pk=pk)
except m.Version.DoesNotExist: except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) 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) file = utils.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(data['alias']) filename = utils.filename_for_schema(data['alias'])
response = HttpResponse(file, content_type='application/zip') response = HttpResponse(file, content_type='application/zip')

View File

@ -79,6 +79,8 @@ INSTALLED_APPS = [
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar', 'drf_spectacular_sidecar',
] ]
if DEBUG:
INSTALLED_APPS.append('django_extensions')
REST_FRAMEWORK = { REST_FRAMEWORK = {
@ -128,7 +130,6 @@ APPEND_SLASH = False
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/ # https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static')) STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
STATIC_URL = 'static/' STATIC_URL = 'static/'
MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
@ -156,7 +157,6 @@ WSGI_APPLICATION = 'project.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases # https://docs.djangoproject.com/en/4.1/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'), 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'),
@ -198,7 +198,6 @@ SPECTACULAR_SETTINGS = {
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS: list[str] = [ AUTH_PASSWORD_VALIDATORS: list[str] = [
# NOTE: Password validators disabled # NOTE: Password validators disabled
# { # {
@ -231,6 +230,14 @@ USE_TZ = True
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 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 = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,

View File

@ -14,6 +14,7 @@ psycopg2-binary
gunicorn gunicorn
djangorestframework-stubs[compatible-mypy] djangorestframework-stubs[compatible-mypy]
django-extensions
mypy mypy
pylint pylint
coverage coverage

View File

@ -50,10 +50,6 @@ def typificationInvalidStr():
return 'Invalid typification string' return 'Invalid typification string'
def libraryTypeUnexpected():
return 'Attempting to use invalid adaptor for non-RSForm item'
def exteorFileVersionNotSupported(): def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'

View File

@ -12,12 +12,13 @@ WORKDIR /result
RUN npm install -g typescript vite RUN npm install -g typescript vite
COPY package.json package-lock.json ./
RUN npm ci
COPY ./ ./ COPY ./ ./
COPY ./env/.env.$BUILD_TYPE ./ COPY ./env/.env.$BUILD_TYPE ./
RUN rm -rf ./env RUN rm -rf ./env
RUN npm ci
ENV NODE_ENV=production ENV NODE_ENV=production
RUN npm run build RUN npm run build

View File

@ -15,6 +15,7 @@
"axios": "^1.7.2", "axios": "^1.7.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.3.8", "framer-motion": "^11.3.8",
"html-to-image": "^1.11.11",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -6808,6 +6809,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",

View File

@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "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", "test": "jest",
"dev": "vite --host", "dev": "vite --host",
"build": "tsc && vite build", "build": "tsc && vite build",
@ -19,6 +19,7 @@
"axios": "^1.7.2", "axios": "^1.7.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.3.8", "framer-motion": "^11.3.8",
"html-to-image": "^1.11.11",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

View File

@ -7,17 +7,16 @@ import { toast } from 'react-toastify';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language'; import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { import { ILibraryItem, ILibraryUpdateData, ITargetAccessPolicy, ITargetLocation, IVersionData } from '@/models/library';
AccessPolicy,
ILibraryItem,
ILibraryUpdateData,
ITargetAccessPolicy,
ITargetLocation,
IVersionData,
LibraryItemType
} from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss'; import {
ICstSubstituteData,
IOperationCreateData,
IOperationCreatedResponse,
IOperationSchemaData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
IConstituentaMeta, IConstituentaMeta,
@ -25,7 +24,6 @@ import {
ICstCreatedResponse, ICstCreatedResponse,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IProduceStructureResponse, 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>) { export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) { if (!version) {
AxiosGet({ 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({ AxiosPost({
endpoint: `/api/rsforms/${schema}/cst-create`, endpoint: `/api/rsforms/${schema}/cst-create`,
request: request 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>) { export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({ AxiosPost({
endpoint: `/api/cctext/inflect`, endpoint: `/api/cctext/inflect`,

View File

@ -38,6 +38,7 @@ export { LuFolderClosed as IconFolderClosed } from 'react-icons/lu';
export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu'; export { LuFolderDot as IconFolderEmpty } from 'react-icons/lu';
export { LuLightbulb as IconHelp } from 'react-icons/lu'; export { LuLightbulb as IconHelp } from 'react-icons/lu';
export { LuLightbulbOff as IconHelpOff } 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 { RiPushpinFill as IconPin } from 'react-icons/ri';
export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; export { RiUnpinLine as IconUnpin } from 'react-icons/ri';
export { BiCaretDown as IconSortDesc } from 'react-icons/bi'; export { BiCaretDown as IconSortDesc } from 'react-icons/bi';

View File

@ -8,7 +8,7 @@ import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; 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 { describeConstituenta } from '@/utils/labels';
import { import {
@ -34,11 +34,11 @@ interface PickSubstitutionsProps {
filter1?: (cst: IConstituenta) => boolean; filter1?: (cst: IConstituenta) => boolean;
filter2?: (cst: IConstituenta) => boolean; filter2?: (cst: IConstituenta) => boolean;
items: ISubstitution[]; items: ISingleSubstitution[];
setItems: React.Dispatch<React.SetStateAction<ISubstitution[]>>; setItems: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
function SubstitutionIcon({ item }: { item: ISubstitution }) { function SubstitutionIcon({ item }: { item: ISingleSubstitution }) {
if (item.deleteRight) { if (item.deleteRight) {
if (item.takeLeftTerm) { if (item.takeLeftTerm) {
return <IconPageRight size='1.2rem' />; return <IconPageRight size='1.2rem' />;
@ -54,7 +54,7 @@ function SubstitutionIcon({ item }: { item: ISubstitution }) {
} }
} }
const columnHelper = createColumnHelper<ISubstitution>(); const columnHelper = createColumnHelper<ISingleSubstitution>();
function PickSubstitutions({ function PickSubstitutions({
items, items,
@ -80,7 +80,7 @@ function PickSubstitutions({
if (!leftCst || !rightCst) { if (!leftCst || !rightCst) {
return; return;
} }
const newSubstitution: ISubstitution = { const newSubstitution: ISingleSubstitution = {
leftCst: leftCst, leftCst: leftCst,
rightCst: rightCst, rightCst: rightCst,
deleteRight: deleteRight, deleteRight: deleteRight,
@ -99,7 +99,7 @@ function PickSubstitutions({
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(row: number) => { (row: number) => {
setItems(prev => { setItems(prev => {
const newItems: ISubstitution[] = []; const newItems: ISingleSubstitution[] = [];
prev.forEach((item, index) => { prev.forEach((item, index) => {
if (index !== row) { if (index !== row) {
newItems.push(item); newItems.push(item);

View 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;

View File

@ -25,8 +25,8 @@ function TextArea({
<div <div
className={clsx( className={clsx(
{ {
'flex flex-col gap-2': !dense, 'flex flex-col flex-grow gap-2': !dense,
'flex items-center gap-3': dense 'flex flex-grow items-center gap-3': dense
}, },
dense && className dense && className
)} )}

View File

@ -5,18 +5,21 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react
import { import {
type DataCallback, type DataCallback,
deleteUnsubscribe, deleteUnsubscribe,
patchDeleteOperation,
patchEditorsSet as patchSetEditors, patchEditorsSet as patchSetEditors,
patchLibraryItem, patchLibraryItem,
patchSetAccessPolicy, patchSetAccessPolicy,
patchSetLocation, patchSetLocation,
patchSetOwner, patchSetOwner,
patchUpdatePositions,
postCreateOperation,
postSubscribe postSubscribe
} from '@/app/backendAPI'; } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails'; import useOssDetails from '@/hooks/useOssDetails';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } 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 { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
@ -43,6 +46,10 @@ interface IOssContext {
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void; setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void; setLocation: (newLocation: string, callback?: () => void) => void;
setEditors: (newEditors: UserID[], 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); const OssContext = createContext<IOssContext | null>(null);
@ -63,13 +70,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const { const {
schema: schema, // prettier: split lines schema, // prettier: split lines
error: errorLoading, error: errorLoading,
setSchema, setSchema,
loading loading
} = useOssDetails({ } = useOssDetails({ target: itemID });
target: itemID
});
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined); const [processingError, setProcessingError] = useState<ErrorData>(undefined);
@ -249,6 +254,59 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, schema] [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 ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
@ -267,7 +325,11 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
setOwner, setOwner,
setEditors, setEditors,
setAccessPolicy, setAccessPolicy,
setLocation setLocation,
savePositions,
createOperation,
deleteOperation
}} }}
> >
{children} {children}

View File

@ -24,14 +24,15 @@ import {
patchSubstituteConstituents, patchSubstituteConstituents,
patchUploadTRS, patchUploadTRS,
patchVersion, patchVersion,
postCreateConstituenta,
postCreateVersion, postCreateVersion,
postNewConstituenta,
postSubscribe postSubscribe
} from '@/app/backendAPI'; } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library'; import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import { import {
ConstituentaID, ConstituentaID,
IConstituentaList, IConstituentaList,
@ -39,7 +40,6 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IRSForm, IRSForm,
@ -399,7 +399,7 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
const cstCreate = useCallback( const cstCreate = useCallback(
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => { (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined); setProcessingError(undefined);
postNewConstituenta(itemID, { postCreateConstituenta(itemID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -8,7 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import useRSFormDetails from '@/hooks/useRSFormDetails'; import useRSFormDetails from '@/hooks/useRSFormDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IInlineSynthesisData, IRSForm, ISubstitution } from '@/models/rsform'; import { IInlineSynthesisData, IRSForm, ISingleSubstitution } from '@/models/rsform';
import TabConstituents from './TabConstituents'; import TabConstituents from './TabConstituents';
import TabSchema from './TabSchema'; import TabSchema from './TabSchema';
@ -30,7 +30,7 @@ function DlgInlineSynthesis({ hideWindow, receiver, onInlineSynthesis }: DlgInli
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined); const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const [selected, setSelected] = useState<LibraryItemID[]>([]); const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]); const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined }); const source = useRSFormDetails({ target: donorID ? String(donorID) : undefined });

View File

@ -2,7 +2,7 @@
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import DataLoader from '@/components/wrap/DataLoader'; 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 { prefixes } from '@/utils/constants';
import PickSubstitutions from '../../components/select/PickSubstitutions'; import PickSubstitutions from '../../components/select/PickSubstitutions';
@ -15,8 +15,8 @@ interface TabSubstitutionsProps {
loading?: boolean; loading?: boolean;
error?: ErrorData; error?: ErrorData;
substitutions: ISubstitution[]; substitutions: ISingleSubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ISingleSubstitution[]>>;
} }
function TabSubstitutions({ function TabSubstitutions({

View File

@ -6,7 +6,8 @@ import { useMemo, useState } from 'react';
import PickSubstitutions from '@/components/select/PickSubstitutions'; import PickSubstitutions from '@/components/select/PickSubstitutions';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { useRSForm } from '@/context/RSFormContext'; import { 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'; import { prefixes } from '@/utils/constants';
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> { interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
@ -16,7 +17,7 @@ interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) { function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]); const [substitutions, setSubstitutions] = useState<ISingleSubstitution[]>([]);
const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]); const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);

View File

@ -2,22 +2,40 @@
* Module: OSS data loading and processing. * 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}. * Loads data into an {@link IOperationSchema} based on {@link IOperationSchemaData}.
* *
*/ */
export class OssLoader { export class OssLoader {
private schema: IOperationSchemaData; private oss: IOperationSchemaData;
private graph: Graph = new Graph();
private operationByID: Map<OperationID, IOperation> = new Map();
constructor(input: IOperationSchemaData) { constructor(input: IOperationSchemaData) {
this.schema = input; this.oss = input;
} }
produceOSS(): IOperationSchema { produceOSS(): IOperationSchema {
const result = this.schema as IOperationSchema; const result = this.oss as IOperationSchema;
result.producedData = [1, 2, 3]; // TODO: put data processing here this.prepareLookups();
this.createGraph();
result.operationByID = this.operationByID;
result.graph = this.graph;
return result; 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));
}
} }

View File

@ -177,3 +177,11 @@ export interface GraphFilterParams {
allowConstant: boolean; allowConstant: boolean;
allowTheorem: boolean; allowTheorem: boolean;
} }
/**
* Represents XY Position.
*/
export interface Position2D {
x: number;
y: number;
}

View File

@ -2,18 +2,125 @@
* Module: Schema of Synthesis Operations. * 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 { 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 { 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;
} }

View 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);
}

View File

@ -5,10 +5,11 @@
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library';
import { ICstSubstitute } from './oss';
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'; import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
/** /**
* Represents Constituenta type. * Represents {@link IConstituenta} type.
*/ */
export enum CstType { export enum CstType {
BASE = 'basic', BASE = 'basic',
@ -21,7 +22,7 @@ export enum CstType {
THEOREM = 'theorem' 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; export const CATEGORY_CST_TYPE = CstType.THEOREM;
/** /**
@ -30,7 +31,7 @@ export const CATEGORY_CST_TYPE = CstType.THEOREM;
export type Position = number; export type Position = number;
/** /**
* Represents {@link Constituenta} identifier type. * Represents {@link IConstituenta} identifier type.
*/ */
export type ConstituentaID = number; 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 export interface ICstCreateData
extends Pick< 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 { export interface ICstMovetoData extends IConstituentaList {
move_to: Position; move_to: Position;
@ -158,32 +159,6 @@ export interface ICstUpdateData
*/ */
export interface ICstRenameData extends ITargetCst, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {} 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}. * Represents data response when creating {@link IConstituenta}.
*/ */
@ -265,6 +240,16 @@ export interface IVersionCreatedResponse {
schema: IRSFormData; 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. * Represents input data for inline synthesis.
*/ */

View File

@ -2,19 +2,22 @@
import { ReactFlowProvider } from 'reactflow'; 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'; import OssFlow from './OssFlow';
function EditorOssGraph() { interface EditorOssGraphProps {
const controller = useOssEdit(); isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
}
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
const [showGrid, setShowGrid] = useLocalStorage<boolean>(storage.ossShowGrid, false);
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<AnimateFade> <OssFlow isModified={isModified} setIsModified={setIsModified} showGrid={showGrid} setShowGrid={setShowGrid} />
<OssFlow controller={controller} />
</AnimateFade>
</ReactFlowProvider> </ReactFlowProvider>
); );
} }

View File

@ -1,56 +1,45 @@
import { CiSquareRemove } from 'react-icons/ci';
import { PiPlugsConnected } from 'react-icons/pi';
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { IconRSForm } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay';
import { useOSS } from '@/context/OssContext';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface InputNodeProps { interface InputNodeProps {
id: string; id: string;
data: {
label: string;
};
} }
function InputNode({ id }: InputNodeProps) { function InputNode({ id, data }: InputNodeProps) {
const controller = useOssEdit(); const controller = useOssEdit();
console.log(controller.isMutable); const model = useOSS();
const handleDelete = () => { const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
console.log('delete node ' + id);
};
const handleClick = () => { const handleOpenSchema = () => {
// controller.selectNode(id); controller.openOperationSchema(Number(id));
// controller.showSelectInput();
}; };
return ( return (
<> <>
<Handle type='target' position={Position.Bottom} /> <Handle type='source' position={Position.Bottom} />
<div>
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
<MiniButton <MiniButton
className='float-right' icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
icon={<CiSquareRemove className='icon-red' />} noHover
title='Удалить' title='Связанная КС'
onClick={handleDelete} onClick={() => {
color={'red'} handleOpenSchema();
}}
disabled={!hasFile}
/> />
<div> </Overlay>
Тип: <strong>Ввод</strong> <div className='flex-grow text-center text-sm'>{data.label}</div>
</div>
<div>
{/* Схема:{controller.getBind(id) === undefined ? '' : controller.getBind(id)} */}
<strong>
<MiniButton
className='float-right'
icon={<PiPlugsConnected className='icon-green' />}
title='Привязать схему'
onClick={() => {
handleClick();
}}
/>
</strong>
</div>
</div>
</> </>
); );
} }

View File

@ -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 { Handle, Position } from 'reactflow';
import { IconRSForm } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton.tsx'; import MiniButton from '@/components/ui/MiniButton.tsx';
import Overlay from '@/components/ui/Overlay';
import { useOSS } from '@/context/OssContext';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
interface OperationNodeProps { interface OperationNodeProps {
id: string; id: string;
data: {
label: string;
};
} }
function OperationNode({ id }: OperationNodeProps) { function OperationNode({ id, data }: OperationNodeProps) {
const controller = useOssEdit(); const controller = useOssEdit();
console.log(controller.isMutable); const model = useOSS();
const handleDelete = () => { const hasFile = !!model.schema?.operationByID.get(Number(id))?.result;
console.log('delete node ' + id);
// onDelete(id);
};
const handleEditOperation = () => { const handleOpenSchema = () => {
console.log('edit operation ' + id); controller.openOperationSchema(Number(id));
//controller.selectNode(id);
//controller.showSynthesis();
};
const handleRunOperation = () => {
console.log('run operation');
// controller.singleSynthesis(id);
}; };
return ( return (
<> <>
<Handle type='target' position={Position.Bottom} /> <Handle type='source' 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.Top} id='a' style={{ left: 50 }} /> <Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'>
<Handle type='source' position={Position.Top} id='b' style={{ right: 50, left: 'auto' }} /> <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' }} />
</> </>
); );
} }

View File

@ -1,49 +1,248 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { toSvg } from 'html-to-image';
import { NodeTypes, ProOptions, ReactFlow } from 'reactflow'; 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 { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useOSS } from '@/context/OssContext'; 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 InputNode from './InputNode';
import OperationNode from './OperationNode'; import OperationNode from './OperationNode';
import ToolbarOssGraph from './ToolbarOssGraph';
const OssNodeTypes: NodeTypes = {
synthesis: OperationNode,
input: InputNode
};
interface OssFlowProps { interface OssFlowProps {
controller: IOssEditContext; isModified: boolean;
setIsModified: React.Dispatch<React.SetStateAction<boolean>>;
showGrid: boolean;
setShowGrid: React.Dispatch<React.SetStateAction<boolean>>;
} }
function OssFlow({ controller }: OssFlowProps) { function OssFlow({ isModified, setIsModified, showGrid, setShowGrid }: OssFlowProps) {
const { calculateHeight } = useConceptOptions(); const { calculateHeight, colors } = useConceptOptions();
const model = useOSS(); const model = useOSS();
const controller = useOssEdit();
const flow = useReactFlow();
console.log(model.loading); const [nodes, setNodes, onNodesChange] = useNodesState([]);
console.log(controller.isMutable); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false);
const initialNodes = [ const onSelectionChange = useCallback(
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' }, type: 'input' }, ({ nodes }: { nodes: Node[] }) => {
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' }, type: 'synthesis' } controller.setSelected(nodes.map(node => Number(node.id)));
]; console.log(nodes);
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; },
[controller]
);
const proOptions: ProOptions = { hideAttribution: true }; useOnSelectionChange({
onChange: onSelectionChange
});
const canvasWidth = useMemo(() => { useLayoutEffect(() => {
return 'calc(100vw - 1rem)'; 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 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 ( return (
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}> <AnimateFade tabIndex={-1} onKeyDown={handleKeyDown}>
<ReactFlow nodes={initialNodes} edges={initialEdges} fitView proOptions={proOptions} nodeTypes={OssNodeTypes} /> <Overlay position='top-0 pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
</div> <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 }}>
{graph}
</div>
</AnimateFade>
); );
} }

View File

@ -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;

View File

@ -4,19 +4,24 @@ import { AnimatePresence } from 'framer-motion';
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditEditors from '@/dialogs/DlgEditEditors';
import { AccessPolicy } from '@/models/library'; 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 { UserID, UserLevel } from '@/models/user';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
export interface IOssEditContext { export interface IOssEditContext {
schema?: IOperationSchema; schema?: IOperationSchema;
selected: OperationID[];
isMutable: boolean; isMutable: boolean;
isProcessing: boolean; isProcessing: boolean;
@ -27,7 +32,15 @@ export interface IOssEditContext {
promptLocation: () => void; promptLocation: () => void;
toggleSubscribe: () => void; toggleSubscribe: () => void;
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
share: () => void; 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); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -41,11 +54,13 @@ export const useOssEdit = () => {
interface OssEditStateProps { interface OssEditStateProps {
// isModified: boolean; // isModified: boolean;
selected: OperationID[];
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
children: React.ReactNode; children: React.ReactNode;
} }
export const OssEditState = ({ children }: OssEditStateProps) => { export const OssEditState = ({ selected, setSelected, children }: OssEditStateProps) => {
// const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const { adminMode } = useConceptOptions(); const { adminMode } = useConceptOptions();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
@ -59,6 +74,10 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
const [showEditEditors, setShowEditEditors] = useState(false); const [showEditEditors, setShowEditEditors] = useState(false);
const [showEditLocation, setShowEditLocation] = 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( useLayoutEffect(
() => () =>
setAccessLevel(prev => { setAccessLevel(prev => {
@ -136,10 +155,62 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
[model] [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 ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
schema: model.schema, schema: model.schema,
selected,
isMutable, isMutable,
isProcessing: model.processing, isProcessing: model.processing,
@ -149,7 +220,13 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
promptEditors, promptEditors,
promptLocation, promptLocation,
share share,
setSelected,
openOperationSchema,
savePositions,
promptCreateOperation,
deleteOperation
}} }}
> >
{model.schema ? ( {model.schema ? (
@ -168,6 +245,15 @@ export const OssEditState = ({ children }: OssEditStateProps) => {
onChangeLocation={handleSetLocation} onChangeLocation={handleSetLocation}
/> />
) : null} ) : null}
{showCreateOperation ? (
<DlgCreateOperation
hideWindow={() => setShowCreateOperation(false)}
oss={model.schema}
positions={positions}
insertPosition={insertPosition}
onCreate={handleCreateOperation}
/>
) : null}
</AnimatePresence> </AnimatePresence>
) : null} ) : null}

View File

@ -17,6 +17,7 @@ import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext'; import { useOSS } from '@/context/OssContext';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { OperationID } from '@/models/oss';
import { information, prompts } from '@/utils/labels'; import { information, prompts } from '@/utils/labels';
import EditorRSForm from './EditorOssCard'; import EditorRSForm from './EditorOssCard';
@ -39,6 +40,7 @@ function OssTabs() {
const { destroyItem } = useLibrary(); const { destroyItem } = useLibrary();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const [selected, setSelected] = useState<OperationID[]>([]);
useBlockNavigation(isModified); useBlockNavigation(isModified);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -112,14 +114,14 @@ function OssTabs() {
const graphPanel = useMemo( const graphPanel = useMemo(
() => ( () => (
<TabPanel> <TabPanel>
<EditorTermGraph /> <EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
</TabPanel> </TabPanel>
), ),
[] [isModified]
); );
return ( return (
<OssEditState> <OssEditState selected={selected} setSelected={setSelected}>
{loading ? <Loader /> : null} {loading ? <Loader /> : null}
{errorLoading ? <ProcessError error={errorLoading} /> : null} {errorLoading ? <ProcessError error={errorLoading} /> : null}
{schema && !loading ? ( {schema && !loading ? (

View File

@ -311,7 +311,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
showParamsDialog={() => setShowParamsDialog(true)} showParamsDialog={() => setShowParamsDialog(true)}
onCreate={handleCreateCst} onCreate={handleCreateCst}
onDelete={handleDeleteCst} onDelete={handleDeleteCst}
onResetViewpoint={() => setToggleResetView(prev => !prev)} onFitView={() => setToggleResetView(prev => !prev)}
onSaveImage={handleSaveImage} onSaveImage={handleSaveImage}
toggleOrbit={() => setOrbit(prev => !prev)} toggleOrbit={() => setOrbit(prev => !prev)}
toggleFoldDerived={handleFoldDerived} toggleFoldDerived={handleFoldDerived}

View File

@ -29,7 +29,7 @@ interface ToolbarTermGraphProps {
showParamsDialog: () => void; showParamsDialog: () => void;
onCreate: () => void; onCreate: () => void;
onDelete: () => void; onDelete: () => void;
onResetViewpoint: () => void; onFitView: () => void;
onSaveImage: () => void; onSaveImage: () => void;
toggleFoldDerived: () => void; toggleFoldDerived: () => void;
@ -48,7 +48,7 @@ function ToolbarTermGraph({
showParamsDialog, showParamsDialog,
onCreate, onCreate,
onDelete, onDelete,
onResetViewpoint, onFitView,
onSaveImage onSaveImage
}: ToolbarTermGraphProps) { }: ToolbarTermGraphProps) {
const controller = useRSEdit(); const controller = useRSEdit();
@ -63,7 +63,7 @@ function ToolbarTermGraph({
<MiniButton <MiniButton
icon={<IconFitImage size='1.25rem' className='icon-primary' />} icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Граф целиком' title='Граф целиком'
onClick={onResetViewpoint} onClick={onFitView}
/> />
<MiniButton <MiniButton
title={!noText ? 'Скрыть текст' : 'Отобразить текст'} title={!noText ? 'Скрыть текст' : 'Отобразить текст'}

View File

@ -25,6 +25,7 @@ import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst'; import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library'; import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import { import {
ConstituentaID, ConstituentaID,
CstType, CstType,
@ -33,7 +34,6 @@ import {
ICstCreateData, ICstCreateData,
ICstMovetoData, ICstMovetoData,
ICstRenameData, ICstRenameData,
ICstSubstituteData,
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IRSForm, IRSForm,

View File

@ -34,21 +34,63 @@
} }
} }
.Flow { .react-flow__handle {
flex-grow: 1; cursor: default !important;
font-size: 12px;
border-color: var(--cl-bg-40);
background-color: var(--cl-bg-120);
.selected & {
border-color: var(--cd-bg-40);
}
.dark & {
border-color: var(--cd-bg-40);
background-color: var(--cd-bg-120);
.selected & {
border-color: var(--cl-bg-40);
}
}
} }
.react-flow__node-input { .react-flow__pane {
border: 1px solid #555; cursor: default;
padding: 10px; }
:is(.react-flow__node-input, .react-flow__node-synthesis) {
cursor: pointer;
border: 1px solid;
padding: 2px;
width: 150px; width: 150px;
border-radius: 5px;
}
.react-flow__node-synthesis {
border: 1px solid #555;
padding: 10px;
width: 250px;
border-radius: 5px; 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;
}
&.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);
}
}
} }

View File

@ -11,6 +11,10 @@ export const PARAMETER = {
refreshTimeout: 100, // milliseconds delay for post-refresh actions refreshTimeout: 100, // milliseconds delay for post-refresh actions
minimalTimeout: 10, // milliseconds delay for fast updates 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 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 graphHoverYLimit: 0.6, // ratio to clientHeight used to determine which side of screen popup should be
graphPopupDelay: 500, // milliseconds delay for graph popup selections graphPopupDelay: 500, // milliseconds delay for graph popup selections
@ -108,6 +112,8 @@ export const storage = {
rsgraphSizing: 'rsgraph.sizing', rsgraphSizing: 'rsgraph.sizing',
rsgraphFoldHidden: 'rsgraph.fold_hidden', rsgraphFoldHidden: 'rsgraph.fold_hidden',
ossShowGrid: 'oss.show_grid',
cstFilterMatch: 'cst.filter.match', cstFilterMatch: 'cst.filter.match',
cstFilterGraph: 'cst.filter.graph' cstFilterGraph: 'cst.filter.graph'
}; };

View File

@ -10,6 +10,7 @@ import { GramData, Grammeme, ReferenceType } from '@/models/language';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library'; import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { validateLocation } from '@/models/libraryAPI'; import { validateLocation } from '@/models/libraryAPI';
import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous'; import { CstMatchMode, DependencyMode, GraphColoring, GraphSizing, HelpTopic } from '@/models/miscellaneous';
import { OperationType } from '@/models/oss';
import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform'; import { CstClass, CstType, ExpressionStatus, IConstituenta, IRSForm } from '@/models/rsform';
import { import {
IArgumentInfo, 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. * UI info descriptors.
*/ */
@ -909,12 +932,14 @@ export const information = {
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`, addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
newLibraryItem: 'Схема успешно создана', newLibraryItem: 'Схема успешно создана',
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newVersion: (version: string) => `Версия создана: ${version}`, newVersion: (version: string) => `Версия создана: ${version}`,
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`, renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`,
versionDestroyed: 'Версия удалена', versionDestroyed: 'Версия удалена',
itemDestroyed: 'Схема удалена', itemDestroyed: 'Схема удалена',
operationDestroyed: 'Операция удалена',
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}` constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
}; };
@ -923,7 +948,8 @@ export const information = {
*/ */
export const errors = { export const errors = {
astFailed: 'Невозможно построить дерево разбора', astFailed: 'Невозможно построить дерево разбора',
passwordsMismatch: 'Пароли не совпадают' passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения'
}; };
/** /**

16
scripts/dev/GraphDB.ps1 Normal file
View 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

View File

@ -2,7 +2,7 @@
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend" $backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
function RunLinters() { function RunCoverage() {
BackendCoverage BackendCoverage
} }
@ -20,4 +20,4 @@ function BackendCoverage() {
Start-Process "$backend\htmlcov\index.html" Start-Process "$backend\htmlcov\index.html"
} }
RunLinters RunCoverage