Compare commits
9 Commits
f91f42ff5b
...
338ad2bb98
Author | SHA1 | Date | |
---|---|---|---|
![]() |
338ad2bb98 | ||
![]() |
b1491ccd35 | ||
![]() |
8977c0fadc | ||
![]() |
f277ce288b | ||
![]() |
c91ff51afa | ||
![]() |
fbaf17ea58 | ||
![]() |
b9ab054a00 | ||
![]() |
7b39b76498 | ||
![]() |
286abaf476 |
|
@ -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
1
.gitignore
vendored
|
@ -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
25
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"jgclark.vscode-todo-highlight",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"alexcvzz.vscode-sqlite",
|
||||||
|
"fractalbrew.backticks",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"streetsidesoftware.code-spell-checker-russian",
|
||||||
|
"kamikillerto.vscode-colorize",
|
||||||
|
"batisteo.vscode-django",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"seyyedkhandon.firacode",
|
||||||
|
"ms-python.isort",
|
||||||
|
"ms-vscode.powershell",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-python.python",
|
||||||
|
"ms-python.debugpy",
|
||||||
|
"ms-python.pylint",
|
||||||
|
"ms-python.autopep8",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
|
"vscode-icons-team.vscode-icons"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
|
@ -5,6 +5,7 @@
|
||||||
"version": "0.2.0",
|
"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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-07-22 13:23
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oss', '0001_initial'),
|
||||||
|
('rsform', '0008_alter_libraryitem_item_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OperationSchema',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
'indexes': [],
|
||||||
|
'constraints': [],
|
||||||
|
},
|
||||||
|
bases=('rsform.libraryitem',),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='operation',
|
||||||
|
name='oss',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='oss.operationschema', verbose_name='Схема синтеза'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -24,4 +24,4 @@ class Argument(Model):
|
||||||
unique_together = [['operation', 'argument']]
|
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}'
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
|
@ -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}'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
''' Tests for Django Models. '''
|
''' Tests for Django Models. '''
|
||||||
|
from .t_Argument import *
|
||||||
|
from .t_Operation import *
|
||||||
|
from .t_SynthesisSubstitution import *
|
||||||
|
|
36
rsconcept/backend/apps/oss/tests/s_models/t_Argument.py
Normal file
36
rsconcept/backend/apps/oss/tests/s_models/t_Argument.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
''' Testing models: Argument. '''
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from apps.oss.models import Argument, Operation, OperationSchema, OperationType
|
||||||
|
|
||||||
|
|
||||||
|
class TestArgument(TestCase):
|
||||||
|
''' Testing Argument model. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.oss = OperationSchema.objects.create(alias='T1')
|
||||||
|
|
||||||
|
self.operation1 = Operation.objects.create(oss=self.oss, alias='KS1', operation_type=OperationType.INPUT)
|
||||||
|
self.operation2 = Operation.objects.create(oss=self.oss, alias='KS2', operation_type=OperationType.SYNTHESIS)
|
||||||
|
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.INPUT)
|
||||||
|
self.argument = Argument.objects.create(
|
||||||
|
operation=self.operation2,
|
||||||
|
argument=self.operation1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
testStr = f'{self.operation1} -> {self.operation2}'
|
||||||
|
self.assertEqual(str(self.argument), testStr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_delete_operation(self):
|
||||||
|
self.assertEqual(Argument.objects.count(), 1)
|
||||||
|
self.operation2.delete()
|
||||||
|
self.assertEqual(Argument.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_delete_argument(self):
|
||||||
|
self.assertEqual(Argument.objects.count(), 1)
|
||||||
|
self.operation1.delete()
|
||||||
|
self.assertEqual(Argument.objects.count(), 0)
|
31
rsconcept/backend/apps/oss/tests/s_models/t_Operation.py
Normal file
31
rsconcept/backend/apps/oss/tests/s_models/t_Operation.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
''' Testing models: Operation. '''
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
|
|
||||||
|
|
||||||
|
class TestOperation(TestCase):
|
||||||
|
''' Testing Operation model. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.oss = OperationSchema.objects.create(alias='T1')
|
||||||
|
self.operation = Operation.objects.create(
|
||||||
|
oss=self.oss,
|
||||||
|
alias='KS1'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
testStr = 'Операция KS1'
|
||||||
|
self.assertEqual(str(self.operation), testStr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_default(self):
|
||||||
|
self.assertEqual(self.operation.oss, self.oss)
|
||||||
|
self.assertEqual(self.operation.operation_type, OperationType.INPUT)
|
||||||
|
self.assertEqual(self.operation.result, None)
|
||||||
|
self.assertEqual(self.operation.alias, 'KS1')
|
||||||
|
self.assertEqual(self.operation.title, '')
|
||||||
|
self.assertEqual(self.operation.comment, '')
|
||||||
|
self.assertEqual(self.operation.position_x, 0)
|
||||||
|
self.assertEqual(self.operation.position_y, 0)
|
|
@ -0,0 +1,75 @@
|
||||||
|
''' Testing models: SynthesisSubstitution. '''
|
||||||
|
from unittest import result
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from apps.oss.models import (
|
||||||
|
Argument,
|
||||||
|
Operation,
|
||||||
|
OperationSchema,
|
||||||
|
OperationType,
|
||||||
|
SynthesisSubstitution
|
||||||
|
)
|
||||||
|
from apps.rsform.models import RSForm
|
||||||
|
|
||||||
|
|
||||||
|
class TestSynthesisSubstitution(TestCase):
|
||||||
|
''' Testing SynthesisSubstitution model. '''
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.oss = OperationSchema.objects.create(alias='T1')
|
||||||
|
|
||||||
|
self.ks1 = RSForm.objects.create(alias='KS1', title='Test1')
|
||||||
|
self.ks1x1 = self.ks1.insert_new('X1', term_resolved='X1_1')
|
||||||
|
self.ks2 = RSForm.objects.create(alias='KS2', title='Test2')
|
||||||
|
self.ks2x1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||||
|
|
||||||
|
self.operation1 = Operation.objects.create(
|
||||||
|
oss=self.oss,
|
||||||
|
alias='KS1',
|
||||||
|
operation_type=OperationType.INPUT,
|
||||||
|
result=self.ks1)
|
||||||
|
self.operation2 = Operation.objects.create(
|
||||||
|
oss=self.oss,
|
||||||
|
alias='KS2',
|
||||||
|
operation_type=OperationType.INPUT,
|
||||||
|
result=self.ks1)
|
||||||
|
self.operation3 = Operation.objects.create(oss=self.oss, alias='KS3', operation_type=OperationType.SYNTHESIS)
|
||||||
|
Argument.objects.create(
|
||||||
|
operation=self.operation3,
|
||||||
|
argument=self.operation1
|
||||||
|
)
|
||||||
|
Argument.objects.create(
|
||||||
|
operation=self.operation3,
|
||||||
|
argument=self.operation2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.substitution = SynthesisSubstitution.objects.create(
|
||||||
|
operation=self.operation3,
|
||||||
|
original=self.ks1x1,
|
||||||
|
substitution=self.ks2x1,
|
||||||
|
transfer_term=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_str(self):
|
||||||
|
testStr = f'{self.ks1x1} -> {self.ks2x1}'
|
||||||
|
self.assertEqual(str(self.substitution), testStr)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_delete_operation(self):
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||||
|
self.operation3.delete()
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_delete_original(self):
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||||
|
self.ks1x1.delete()
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cascade_delete_substitution(self):
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 1)
|
||||||
|
self.ks2x1.delete()
|
||||||
|
self.assertEqual(SynthesisSubstitution.objects.count(), 0)
|
|
@ -12,29 +12,29 @@ class TestOssViewset(EndpointTester):
|
||||||
|
|
||||||
def setUp(self):
|
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)
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-07-22 14:21
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rsform', '0008_alter_libraryitem_item_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RSForm',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
'indexes': [],
|
||||||
|
'constraints': [],
|
||||||
|
},
|
||||||
|
bases=('rsform.libraryitem',),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='constituenta',
|
||||||
|
name='schema',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Концептуальная схема'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='librarytemplate',
|
||||||
|
name='lib_source',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='rsform.rsform', verbose_name='Источник'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -40,7 +40,7 @@ class Constituenta(Model):
|
||||||
''' Constituenta is the base unit for every conceptual schema. '''
|
''' 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(
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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'])
|
|
@ -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 (
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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='люди'
|
||||||
|
|
|
@ -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': [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -14,6 +14,7 @@ psycopg2-binary
|
||||||
gunicorn
|
gunicorn
|
||||||
|
|
||||||
djangorestframework-stubs[compatible-mypy]
|
djangorestframework-stubs[compatible-mypy]
|
||||||
|
django-extensions
|
||||||
mypy
|
mypy
|
||||||
pylint
|
pylint
|
||||||
coverage
|
coverage
|
|
@ -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 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
7
rsconcept/frontend/package-lock.json
generated
7
rsconcept/frontend/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
58
rsconcept/frontend/src/components/select/SelectOperation.tsx
Normal file
58
rsconcept/frontend/src/components/select/SelectOperation.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { IOperation, OperationID } from '@/models/oss';
|
||||||
|
import { matchOperation } from '@/models/ossAPI';
|
||||||
|
|
||||||
|
import { CProps } from '../props';
|
||||||
|
import SelectSingle from '../ui/SelectSingle';
|
||||||
|
|
||||||
|
interface SelectOperationProps extends CProps.Styling {
|
||||||
|
items?: IOperation[];
|
||||||
|
value?: IOperation;
|
||||||
|
onSelectValue: (newValue?: IOperation) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectOperation({
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
onSelectValue,
|
||||||
|
placeholder = 'Выберите операцию',
|
||||||
|
...restProps
|
||||||
|
}: SelectOperationProps) {
|
||||||
|
const options = useMemo(() => {
|
||||||
|
return (
|
||||||
|
items?.map(cst => ({
|
||||||
|
value: cst.id,
|
||||||
|
label: `${cst.alias}: ${cst.title}`
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const filter = useCallback(
|
||||||
|
(option: { value: OperationID | undefined; label: string }, inputValue: string) => {
|
||||||
|
const operation = items?.find(item => item.id === option.value);
|
||||||
|
return !operation ? false : matchOperation(operation, inputValue);
|
||||||
|
},
|
||||||
|
[items]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectSingle
|
||||||
|
className={clsx('text-ellipsis', className)}
|
||||||
|
options={options}
|
||||||
|
value={value ? { value: value.id, label: `${value.alias}: ${value.title}` } : undefined}
|
||||||
|
onChange={data => onSelectValue(items?.find(cst => cst.id === data?.value))}
|
||||||
|
// @ts-expect-error: TODO: use type definitions from react-select in filter object
|
||||||
|
filterOption={filter}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectOperation;
|
|
@ -25,8 +25,8 @@ function TextArea({
|
||||||
<div
|
<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
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useLayoutEffect, useMemo, useState } from 'react';
|
||||||
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
|
|
||||||
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
import Modal from '@/components/ui/Modal';
|
||||||
|
import Overlay from '@/components/ui/Overlay';
|
||||||
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
|
import { LibraryItemID } from '@/models/library';
|
||||||
|
import { HelpTopic, Position2D } from '@/models/miscellaneous';
|
||||||
|
import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
import { describeOperationType, labelOperationType } from '@/utils/labels';
|
||||||
|
|
||||||
|
import TabInputOperation from './TabInputOperation';
|
||||||
|
import TabSynthesisOperation from './TabSynthesisOperation';
|
||||||
|
|
||||||
|
interface DlgCreateOperationProps {
|
||||||
|
hideWindow: () => void;
|
||||||
|
oss: IOperationSchema;
|
||||||
|
positions: IOperationPosition[];
|
||||||
|
insertPosition: Position2D;
|
||||||
|
onCreate: (data: IOperationCreateData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TabID {
|
||||||
|
INPUT = 0,
|
||||||
|
SYNTHESIS = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function DlgCreateOperation({ hideWindow, oss, insertPosition, positions, onCreate }: DlgCreateOperationProps) {
|
||||||
|
const library = useLibrary();
|
||||||
|
const [activeTab, setActiveTab] = useState(TabID.INPUT);
|
||||||
|
|
||||||
|
const [alias, setAlias] = useState('');
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [comment, setComment] = useState('');
|
||||||
|
const [inputs, setInputs] = useState<OperationID[]>([]);
|
||||||
|
const [attachedID, setAttachedID] = useState<LibraryItemID | undefined>(undefined);
|
||||||
|
|
||||||
|
const isValid = useMemo(() => alias !== '', [alias]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (attachedID) {
|
||||||
|
const schema = library.items.find(value => value.id === attachedID);
|
||||||
|
if (schema) {
|
||||||
|
setAlias(schema.alias);
|
||||||
|
setTitle(schema.title);
|
||||||
|
setComment(schema.comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [attachedID, library]);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
const data: IOperationCreateData = {
|
||||||
|
item_data: {
|
||||||
|
position_x: insertPosition.x,
|
||||||
|
position_y: insertPosition.y,
|
||||||
|
alias: alias,
|
||||||
|
title: title,
|
||||||
|
comment: comment,
|
||||||
|
operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS,
|
||||||
|
result: activeTab === TabID.INPUT ? attachedID ?? null : null
|
||||||
|
},
|
||||||
|
positions: positions,
|
||||||
|
arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined
|
||||||
|
};
|
||||||
|
onCreate(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputPanel = useMemo(
|
||||||
|
() => (
|
||||||
|
<TabPanel>
|
||||||
|
<TabInputOperation
|
||||||
|
alias={alias}
|
||||||
|
setAlias={setAlias}
|
||||||
|
comment={comment}
|
||||||
|
setComment={setComment}
|
||||||
|
title={title}
|
||||||
|
setTitle={setTitle}
|
||||||
|
attachedID={attachedID}
|
||||||
|
setAttachedID={setAttachedID}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
),
|
||||||
|
[alias, comment, title, attachedID]
|
||||||
|
);
|
||||||
|
|
||||||
|
const synthesisPanel = useMemo(
|
||||||
|
() => (
|
||||||
|
<TabPanel>
|
||||||
|
<TabSynthesisOperation
|
||||||
|
oss={oss}
|
||||||
|
alias={alias}
|
||||||
|
setAlias={setAlias}
|
||||||
|
comment={comment}
|
||||||
|
setComment={setComment}
|
||||||
|
title={title}
|
||||||
|
setTitle={setTitle}
|
||||||
|
setInputs={setInputs}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
),
|
||||||
|
[oss, alias, comment, title]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
header='Создание операции'
|
||||||
|
submitText='Создать'
|
||||||
|
hideWindow={hideWindow}
|
||||||
|
canSubmit={isValid}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className='w-[40rem] px-6 min-h-[35rem]'
|
||||||
|
>
|
||||||
|
<Overlay position='top-0 right-0'>
|
||||||
|
<BadgeHelp topic={HelpTopic.CC_OSS} className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')} offset={14} />
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
selectedTabClassName='clr-selected'
|
||||||
|
className='flex flex-col'
|
||||||
|
selectedIndex={activeTab}
|
||||||
|
onSelect={setActiveTab}
|
||||||
|
>
|
||||||
|
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
|
||||||
|
<TabLabel
|
||||||
|
title={describeOperationType(OperationType.INPUT)}
|
||||||
|
label={labelOperationType(OperationType.INPUT)}
|
||||||
|
/>
|
||||||
|
<TabLabel
|
||||||
|
title={describeOperationType(OperationType.SYNTHESIS)}
|
||||||
|
label={labelOperationType(OperationType.SYNTHESIS)}
|
||||||
|
/>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
{inputPanel}
|
||||||
|
{synthesisPanel}
|
||||||
|
</Tabs>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DlgCreateOperation;
|
|
@ -0,0 +1,65 @@
|
||||||
|
import PickSchema from '@/components/select/PickSchema';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import TextArea from '@/components/ui/TextArea';
|
||||||
|
import TextInput from '@/components/ui/TextInput';
|
||||||
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import { LibraryItemID } from '@/models/library';
|
||||||
|
import { limits, patterns } from '@/utils/constants';
|
||||||
|
|
||||||
|
interface TabInputOperationProps {
|
||||||
|
alias: string;
|
||||||
|
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
title: string;
|
||||||
|
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
comment: string;
|
||||||
|
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
attachedID: LibraryItemID | undefined;
|
||||||
|
setAttachedID: React.Dispatch<React.SetStateAction<LibraryItemID | undefined>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabInputOperation({
|
||||||
|
alias,
|
||||||
|
setAlias,
|
||||||
|
title,
|
||||||
|
setTitle,
|
||||||
|
comment,
|
||||||
|
setComment,
|
||||||
|
attachedID,
|
||||||
|
setAttachedID
|
||||||
|
}: TabInputOperationProps) {
|
||||||
|
return (
|
||||||
|
<AnimateFade className='cc-column'>
|
||||||
|
<TextInput
|
||||||
|
id='operation_title'
|
||||||
|
label='Полное название'
|
||||||
|
value={title}
|
||||||
|
onChange={event => setTitle(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div className='flex gap-6'>
|
||||||
|
<TextInput
|
||||||
|
id='operation_alias'
|
||||||
|
label='Сокращение'
|
||||||
|
className='w-[14rem]'
|
||||||
|
pattern={patterns.library_alias}
|
||||||
|
title={`не более ${limits.library_alias_len} символов`}
|
||||||
|
value={alias}
|
||||||
|
onChange={event => setAlias(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id='operation_comment'
|
||||||
|
label='Описание'
|
||||||
|
noResize
|
||||||
|
rows={3}
|
||||||
|
value={comment}
|
||||||
|
onChange={event => setComment(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label text='Загружаемая концептуальная схема' />
|
||||||
|
<PickSchema value={attachedID} onSelectValue={setAttachedID} rows={8} />
|
||||||
|
</AnimateFade>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabInputOperation;
|
|
@ -0,0 +1,92 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import SelectOperation from '@/components/select/SelectOperation';
|
||||||
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
|
import Label from '@/components/ui/Label';
|
||||||
|
import TextArea from '@/components/ui/TextArea';
|
||||||
|
import TextInput from '@/components/ui/TextInput';
|
||||||
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
|
import { IOperation, IOperationSchema, OperationID } from '@/models/oss';
|
||||||
|
import { limits, patterns } from '@/utils/constants';
|
||||||
|
|
||||||
|
interface TabSynthesisOperationProps {
|
||||||
|
oss: IOperationSchema;
|
||||||
|
alias: string;
|
||||||
|
setAlias: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
title: string;
|
||||||
|
setTitle: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
comment: string;
|
||||||
|
setComment: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setInputs: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabSynthesisOperation({
|
||||||
|
oss,
|
||||||
|
alias,
|
||||||
|
setAlias,
|
||||||
|
title,
|
||||||
|
setTitle,
|
||||||
|
comment,
|
||||||
|
setComment,
|
||||||
|
setInputs
|
||||||
|
}: TabSynthesisOperationProps) {
|
||||||
|
const [left, setLeft] = useState<IOperation | undefined>(undefined);
|
||||||
|
const [right, setRight] = useState<IOperation | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const inputs: OperationID[] = [];
|
||||||
|
if (left) {
|
||||||
|
inputs.push(left.id);
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
inputs.push(right.id);
|
||||||
|
}
|
||||||
|
setInputs(inputs);
|
||||||
|
}, [setInputs, left, right]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimateFade className='cc-column'>
|
||||||
|
<TextInput
|
||||||
|
id='operation_title'
|
||||||
|
label='Полное название'
|
||||||
|
value={title}
|
||||||
|
onChange={event => setTitle(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div className='flex gap-6'>
|
||||||
|
<TextInput
|
||||||
|
id='operation_alias'
|
||||||
|
label='Сокращение'
|
||||||
|
className='w-[14rem]'
|
||||||
|
pattern={patterns.library_alias}
|
||||||
|
title={`не более ${limits.library_alias_len} символов`}
|
||||||
|
value={alias}
|
||||||
|
onChange={event => setAlias(event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
id='operation_comment'
|
||||||
|
label='Описание'
|
||||||
|
noResize
|
||||||
|
rows={3}
|
||||||
|
value={comment}
|
||||||
|
onChange={event => setComment(event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex justify-between'>
|
||||||
|
<FlexColumn>
|
||||||
|
<Label text='Аргумент 1' />
|
||||||
|
<SelectOperation items={oss.items} value={left} onSelectValue={setLeft} />
|
||||||
|
</FlexColumn>
|
||||||
|
<FlexColumn>
|
||||||
|
<Label text='Аргумент 2' className='text-right' />
|
||||||
|
<SelectOperation items={oss.items} value={right} onSelectValue={setRight} />
|
||||||
|
</FlexColumn>
|
||||||
|
</div>
|
||||||
|
</AnimateFade>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabSynthesisOperation;
|
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './DlgCreateOperation';
|
|
@ -8,7 +8,7 @@ import Modal, { ModalProps } from '@/components/ui/Modal';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import 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 });
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
18
rsconcept/frontend/src/models/ossAPI.ts
Normal file
18
rsconcept/frontend/src/models/ossAPI.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Module: API for OperationSystem.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TextMatcher } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { IOperation } from './oss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given target {@link IOperation} matches the specified query using.
|
||||||
|
*
|
||||||
|
* @param target - The target object to be matched.
|
||||||
|
* @param query - The query string used for matching.
|
||||||
|
*/
|
||||||
|
export function matchOperation(target: IOperation, query: string): boolean {
|
||||||
|
const matcher = new TextMatcher(query);
|
||||||
|
return matcher.test(target.alias) || matcher.test(target.title);
|
||||||
|
}
|
|
@ -5,10 +5,11 @@
|
||||||
import { Graph } from '@/models/Graph';
|
import { 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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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' }} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { IconDestroy, IconFitImage, IconGrid, IconImage, IconNewItem, IconReset, IconSave } from '@/components/Icons';
|
||||||
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
import { prepareTooltip } from '@/utils/labels';
|
||||||
|
|
||||||
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
|
interface ToolbarOssGraphProps {
|
||||||
|
isModified: boolean;
|
||||||
|
showGrid: boolean;
|
||||||
|
onCreate: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onFitView: () => void;
|
||||||
|
onSaveImage: () => void;
|
||||||
|
onSavePositions: () => void;
|
||||||
|
onResetPositions: () => void;
|
||||||
|
toggleShowGrid: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarOssGraph({
|
||||||
|
isModified,
|
||||||
|
showGrid,
|
||||||
|
onCreate,
|
||||||
|
onDelete,
|
||||||
|
onFitView,
|
||||||
|
onSaveImage,
|
||||||
|
onSavePositions,
|
||||||
|
onResetPositions,
|
||||||
|
toggleShowGrid
|
||||||
|
}: ToolbarOssGraphProps) {
|
||||||
|
const controller = useOssEdit();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='cc-icons'>
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={controller.isProcessing || !isModified}
|
||||||
|
onClick={onSavePositions}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Сбросить изменения'
|
||||||
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
|
disabled={!isModified}
|
||||||
|
onClick={onResetPositions}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Сбросить вид'
|
||||||
|
onClick={onFitView}
|
||||||
|
/>
|
||||||
|
<MiniButton
|
||||||
|
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||||
|
icon={
|
||||||
|
showGrid ? (
|
||||||
|
<IconGrid size='1.25rem' className='icon-green' />
|
||||||
|
) : (
|
||||||
|
<IconGrid size='1.25rem' className='icon-primary' />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={toggleShowGrid}
|
||||||
|
/>
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Новая операция'
|
||||||
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
|
disabled={controller.isProcessing}
|
||||||
|
onClick={onCreate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{controller.isMutable ? (
|
||||||
|
<MiniButton
|
||||||
|
title='Удалить выбранную'
|
||||||
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
|
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
||||||
|
onClick={onDelete}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<MiniButton
|
||||||
|
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||||
|
title='Сохранить изображение'
|
||||||
|
onClick={onSaveImage}
|
||||||
|
/>
|
||||||
|
<BadgeHelp
|
||||||
|
topic={HelpTopic.UI_OSS_GRAPH}
|
||||||
|
className={clsx(PARAMETER.TOOLTIP_WIDTH, 'sm:max-w-[40rem]')}
|
||||||
|
offset={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToolbarOssGraph;
|
|
@ -4,19 +4,24 @@ import { AnimatePresence } from 'framer-motion';
|
||||||
import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
|
import { 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}
|
||||||
|
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 ? 'Скрыть текст' : 'Отобразить текст'}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
16
scripts/dev/GraphDB.ps1
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generate DOT file for DB structure
|
||||||
|
$backend = Resolve-Path -Path "${PSScriptRoot}\..\..\rsconcept\backend"
|
||||||
|
|
||||||
|
function GenerateDOT() {
|
||||||
|
Set-Location $backend
|
||||||
|
|
||||||
|
$python = "${backend}\venv\Scripts\python.exe"
|
||||||
|
$djangoSrc = "${backend}\manage.py"
|
||||||
|
|
||||||
|
& $python $djangoSrc graph_models -o visualizeDB.dot
|
||||||
|
|
||||||
|
notepad.exe "${backend}\visualizeDB.dot"
|
||||||
|
Start-Process "https://dreampuf.github.io/GraphvizOnline"
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateDOT
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
|
$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
|
Loading…
Reference in New Issue
Block a user