Compare commits
No commits in common. "266fdf0c304ff43ff1a07ce2bde3ab1b184a5ce3" and "679eefcd3854f33873fe8c9d4e1f30165103b62d" have entirely different histories.
266fdf0c30
...
679eefcd38
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -128,7 +128,6 @@
|
|||
"perfectivity",
|
||||
"PNCT",
|
||||
"ponomarev",
|
||||
"popleft",
|
||||
"PRCL",
|
||||
"PRTF",
|
||||
"PRTS",
|
||||
|
|
|
@ -169,7 +169,7 @@ This readme file is used mostly to document project dependencies and conventions
|
|||
|
||||
This is the build for local Development
|
||||
|
||||
- Install Docker Desktop, Python 3.12, NodeJS, VSCode or other compatible IDE
|
||||
- Install Python 3.12, NodeJS, VSCode, Docker Desktop
|
||||
- copy import wheels from ConceptCore to rsconcept/backend/import
|
||||
- run scripts/dev/LocalEnvSetup.ps1
|
||||
- use VSCode configs in root folder to start development
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
''' Admin view: Library. '''
|
||||
from typing import cast
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.LibraryItem)
|
||||
class LibraryItemAdmin(admin.ModelAdmin):
|
||||
''' Admin model: LibraryItem. '''
|
||||
date_hierarchy = 'time_update'
|
||||
|
@ -19,7 +17,6 @@ class LibraryItemAdmin(admin.ModelAdmin):
|
|||
search_fields = ['alias', 'title', 'location']
|
||||
|
||||
|
||||
@admin.register(models.LibraryTemplate)
|
||||
class LibraryTemplateAdmin(admin.ModelAdmin):
|
||||
''' Admin model: LibraryTemplate. '''
|
||||
list_display = ['id', 'alias']
|
||||
|
@ -32,7 +29,6 @@ class LibraryTemplateAdmin(admin.ModelAdmin):
|
|||
return 'N/A'
|
||||
|
||||
|
||||
@admin.register(models.Editor)
|
||||
class EditorAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Editors. '''
|
||||
list_display = ['id', 'item', 'editor']
|
||||
|
@ -42,10 +38,16 @@ class EditorAdmin(admin.ModelAdmin):
|
|||
]
|
||||
|
||||
|
||||
@admin.register(models.Version)
|
||||
class VersionAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Versions. '''
|
||||
list_display = ['id', 'item', 'version', 'description', 'time_create']
|
||||
search_fields = [
|
||||
'item__title', 'item__alias'
|
||||
]
|
||||
|
||||
|
||||
|
||||
admin.site.register(models.LibraryItem, LibraryItemAdmin)
|
||||
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
|
||||
admin.site.register(models.Version, VersionAdmin)
|
||||
admin.site.register(models.Editor, EditorAdmin)
|
||||
|
|
|
@ -9,6 +9,7 @@ from apps.library.models import (
|
|||
LibraryTemplate,
|
||||
LocationHead
|
||||
)
|
||||
from apps.oss.models import OperationSchema
|
||||
from apps.rsform.models import RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
from shared.testing_utils import response_contains
|
||||
|
@ -58,8 +59,8 @@ class TestLibraryViewset(EndpointTester):
|
|||
'read_only': True
|
||||
}
|
||||
response = self.executeCreated(data=data)
|
||||
oss = LibraryItem.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(oss.owner, self.user)
|
||||
oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id']))
|
||||
self.assertEqual(oss.model.owner, self.user)
|
||||
self.assertEqual(response.data['owner'], self.user.pk)
|
||||
self.assertEqual(response.data['item_type'], data['item_type'])
|
||||
self.assertEqual(response.data['title'], data['title'])
|
||||
|
@ -333,12 +334,12 @@ class TestLibraryViewset(EndpointTester):
|
|||
@decl_endpoint('/api/library/{item}/clone', method='post')
|
||||
def test_clone_rsform(self):
|
||||
schema = RSForm(self.owned)
|
||||
x12 = schema.insert_last(
|
||||
x12 = schema.insert_new(
|
||||
alias='X12',
|
||||
term_raw='человек',
|
||||
term_resolved='человек'
|
||||
)
|
||||
d2 = schema.insert_last(
|
||||
d2 = schema.insert_new(
|
||||
alias='D2',
|
||||
term_raw='@{X12|plur}',
|
||||
term_resolved='люди'
|
||||
|
|
|
@ -20,7 +20,7 @@ class TestVersionViews(EndpointTester):
|
|||
self.owned_id = self.owned.model.pk
|
||||
self.unowned = RSForm.create(title='Test2', alias='T2')
|
||||
self.unowned_id = self.unowned.model.pk
|
||||
self.x1 = self.owned.insert_last(
|
||||
self.x1 = self.owned.insert_new(
|
||||
alias='X1',
|
||||
convention='testStart'
|
||||
)
|
||||
|
@ -44,7 +44,7 @@ class TestVersionViews(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/library/{schema}/create-version', method='post')
|
||||
def test_create_version_filter(self):
|
||||
x2 = self.owned.insert_last('X2')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
|
||||
response = self.executeCreated(data=data, schema=self.owned_id)
|
||||
version = Version.objects.get(pk=response.data['version'])
|
||||
|
@ -67,7 +67,7 @@ class TestVersionViews(EndpointTester):
|
|||
self.executeNotFound(schema=self.unowned_id, version=version_id)
|
||||
|
||||
self.owned.model.alias = 'NewName'
|
||||
self.owned.model.save()
|
||||
self.owned.save()
|
||||
self.x1.alias = 'X33'
|
||||
self.x1.save()
|
||||
|
||||
|
@ -84,18 +84,15 @@ class TestVersionViews(EndpointTester):
|
|||
alias='A1',
|
||||
cst_type='axiom',
|
||||
definition_formal='X1=X1',
|
||||
order=1,
|
||||
crucial=True
|
||||
order=1
|
||||
)
|
||||
version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
|
||||
a1.definition_formal = 'X1=X2'
|
||||
a1.crucial = False
|
||||
a1.save()
|
||||
|
||||
response = self.executeOK(schema=self.owned_id, version=version_id)
|
||||
loaded_a1 = response.data['items'][1]
|
||||
self.assertEqual(loaded_a1['definition_formal'], 'X1=X1')
|
||||
self.assertEqual(loaded_a1['crucial'], True)
|
||||
self.assertEqual(loaded_a1['parse']['status'], 'verified')
|
||||
|
||||
|
||||
|
@ -154,14 +151,14 @@ class TestVersionViews(EndpointTester):
|
|||
@decl_endpoint('/api/versions/{version}/restore', method='patch')
|
||||
def test_restore_version(self):
|
||||
x1 = self.x1
|
||||
x2 = self.owned.insert_last('X2')
|
||||
d1 = self.owned.insert_last('D1', term_raw='TestTerm')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
d1 = self.owned.insert_new('D1', term_raw='TestTerm')
|
||||
data = {'version': '1.0.0', 'description': 'test'}
|
||||
version_id = self._create_version(data=data)
|
||||
invalid_id = version_id + 1337
|
||||
|
||||
Constituenta.objects.get(pk=d1.pk).delete()
|
||||
x3 = self.owned.insert_last('X3')
|
||||
self.owned.delete_cst([d1])
|
||||
x3 = self.owned.insert_new('X3')
|
||||
x1.order = x3.order
|
||||
x1.convention = 'Test2'
|
||||
x1.term_raw = 'Test'
|
||||
|
|
|
@ -14,7 +14,7 @@ from rest_framework.request import Request
|
|||
from rest_framework.response import Response
|
||||
|
||||
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
||||
from apps.rsform.models import RSFormCached
|
||||
from apps.rsform.models import RSForm
|
||||
from apps.rsform.serializers import RSFormParseSerializer
|
||||
from apps.users.models import User
|
||||
from shared import permissions
|
||||
|
@ -70,7 +70,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
PropagationFacade.before_delete_schema(instance)
|
||||
super().perform_destroy(instance)
|
||||
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
schemas = list(OperationSchema.owned_schemasQ(instance))
|
||||
schemas = list(OperationSchema(instance).owned_schemas())
|
||||
super().perform_destroy(instance)
|
||||
for schema in schemas:
|
||||
self.perform_destroy(schema)
|
||||
|
@ -159,7 +159,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data['item_data']
|
||||
with transaction.atomic():
|
||||
clone = deepcopy(item)
|
||||
clone.pk = None
|
||||
clone.owner = cast(User, self.request.user)
|
||||
|
@ -170,9 +169,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
clone.read_only = False
|
||||
clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
|
||||
clone.location = data.get('location', m.LocationHead.USER)
|
||||
|
||||
with transaction.atomic():
|
||||
clone.save()
|
||||
need_filter = 'items' in request.data and len(request.data['items']) > 0
|
||||
for cst in RSFormCached(item).constituentsQ():
|
||||
for cst in RSForm(item).constituents():
|
||||
if not need_filter or cst.pk in request.data['items']:
|
||||
cst.pk = None
|
||||
cst.schema = clone
|
||||
|
@ -204,7 +205,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema.owned_schemasQ(item).only('owner')
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('owner')
|
||||
for schema in owned_schemas:
|
||||
schema.owner_id = new_owner
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['owner'])
|
||||
|
@ -238,7 +239,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema.owned_schemasQ(item).only('location')
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('location')
|
||||
for schema in owned_schemas:
|
||||
schema.location = location
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['location'])
|
||||
|
@ -270,7 +271,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
|
||||
with transaction.atomic():
|
||||
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||
owned_schemas = OperationSchema.owned_schemasQ(item).only('access_policy')
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('access_policy')
|
||||
for schema in owned_schemas:
|
||||
schema.access_policy = new_policy
|
||||
m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy'])
|
||||
|
@ -300,7 +301,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
|||
with transaction.atomic():
|
||||
added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
|
||||
if len(added) >= 0 or len(deleted) >= 0:
|
||||
owned_schemas = OperationSchema.owned_schemasQ(item).only('pk')
|
||||
owned_schemas = OperationSchema(item).owned_schemas().only('pk')
|
||||
if owned_schemas.exists():
|
||||
m.Editor.objects.filter(
|
||||
item__in=owned_schemas,
|
||||
|
|
|
@ -47,7 +47,6 @@ class VersionViewset(
|
|||
item = version.item
|
||||
with transaction.atomic():
|
||||
RSFormSerializer(item).restore_from_version(version.data)
|
||||
item.save(update_fields=['time_update'])
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=RSFormParseSerializer(item).data
|
||||
|
@ -70,7 +69,7 @@ def export_file(request: Request, pk: int) -> HttpResponse:
|
|||
version = m.Version.objects.get(pk=pk)
|
||||
except m.Version.DoesNotExist:
|
||||
return Response(status=c.HTTP_404_NOT_FOUND)
|
||||
data = RSFormTRSSerializer.load_versioned_data(version.data)
|
||||
data = RSFormTRSSerializer(version.item).from_versioned_data(version.data)
|
||||
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
||||
filename = utils.filename_for_schema(data['alias'])
|
||||
response = HttpResponse(file, content_type='application/zip')
|
||||
|
|
|
@ -4,7 +4,6 @@ from django.contrib import admin
|
|||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.Operation)
|
||||
class OperationAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation. '''
|
||||
ordering = ['oss']
|
||||
|
@ -20,7 +19,6 @@ class OperationAdmin(admin.ModelAdmin):
|
|||
search_fields = ['id', 'operation_type', 'title', 'alias']
|
||||
|
||||
|
||||
@admin.register(models.Block)
|
||||
class BlockAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Block. '''
|
||||
ordering = ['oss']
|
||||
|
@ -28,7 +26,6 @@ class BlockAdmin(admin.ModelAdmin):
|
|||
search_fields = ['oss']
|
||||
|
||||
|
||||
@admin.register(models.Layout)
|
||||
class LayoutAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Layout. '''
|
||||
ordering = ['oss']
|
||||
|
@ -36,7 +33,6 @@ class LayoutAdmin(admin.ModelAdmin):
|
|||
search_fields = ['oss']
|
||||
|
||||
|
||||
@admin.register(models.Argument)
|
||||
class ArgumentAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Operation arguments. '''
|
||||
ordering = ['operation']
|
||||
|
@ -44,7 +40,6 @@ class ArgumentAdmin(admin.ModelAdmin):
|
|||
search_fields = ['id', 'operation', 'argument']
|
||||
|
||||
|
||||
@admin.register(models.Substitution)
|
||||
class SynthesisSubstitutionAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Substitutions as part of Synthesis operation. '''
|
||||
ordering = ['operation']
|
||||
|
@ -52,7 +47,6 @@ class SynthesisSubstitutionAdmin(admin.ModelAdmin):
|
|||
search_fields = ['id', 'operation', 'original', 'substitution']
|
||||
|
||||
|
||||
@admin.register(models.Inheritance)
|
||||
class InheritanceAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Inheritance. '''
|
||||
ordering = ['operation']
|
||||
|
@ -60,9 +54,9 @@ class InheritanceAdmin(admin.ModelAdmin):
|
|||
search_fields = ['id', 'operation', 'parent', 'child']
|
||||
|
||||
|
||||
@admin.register(models.Reference)
|
||||
class ReferenceAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Reference. '''
|
||||
ordering = ['reference', 'target']
|
||||
list_display = ['id', 'reference', 'target']
|
||||
search_fields = ['id', 'reference', 'target']
|
||||
admin.site.register(models.Operation, OperationAdmin)
|
||||
admin.site.register(models.Block, BlockAdmin)
|
||||
admin.site.register(models.Layout, LayoutAdmin)
|
||||
admin.site.register(models.Argument, ArgumentAdmin)
|
||||
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin)
|
||||
admin.site.register(models.Inheritance, InheritanceAdmin)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.2.4 on 2025-07-31 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0013_alter_layout_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operation',
|
||||
name='operation_type',
|
||||
field=models.CharField(choices=[('input', 'Input'), ('synthesis', 'Synthesis'), ('reference', 'Reference')], default='input', max_length=10, verbose_name='Тип'),
|
||||
),
|
||||
]
|
|
@ -1,27 +0,0 @@
|
|||
# Generated by Django 5.2.4 on 2025-07-31 08:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('oss', '0014_alter_operation_operation_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Reference',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='oss.operation', verbose_name='Отсылка')),
|
||||
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targets', to='oss.operation', verbose_name='Целевая Операция')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Отсылка',
|
||||
'verbose_name_plural': 'Отсылки',
|
||||
'unique_together': {('reference', 'target')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -23,10 +23,3 @@ class Layout(Model):
|
|||
|
||||
def __str__(self) -> str:
|
||||
return f'Схема расположения {self.oss.alias}'
|
||||
|
||||
@staticmethod
|
||||
def update_data(itemID: int, data: dict) -> None:
|
||||
''' Update layout data. '''
|
||||
layout = Layout.objects.get(oss_id=itemID)
|
||||
layout.data = data
|
||||
layout.save()
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
''' Models: Operation in OSS. '''
|
||||
# pylint: disable=duplicate-code
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import (
|
||||
CASCADE,
|
||||
SET_NULL,
|
||||
|
@ -13,10 +11,7 @@ from django.db.models import (
|
|||
TextField
|
||||
)
|
||||
|
||||
from apps.library.models import LibraryItem
|
||||
|
||||
from .Argument import Argument
|
||||
from .Reference import Reference
|
||||
from .Substitution import Substitution
|
||||
|
||||
|
||||
|
@ -24,7 +19,6 @@ class OperationType(TextChoices):
|
|||
''' Type of operation. '''
|
||||
INPUT = 'input'
|
||||
SYNTHESIS = 'synthesis'
|
||||
REFERENCE = 'reference'
|
||||
|
||||
|
||||
class Operation(Model):
|
||||
|
@ -82,37 +76,9 @@ class Operation(Model):
|
|||
return f'Операция {self.alias}'
|
||||
|
||||
def getQ_arguments(self) -> QuerySet[Argument]:
|
||||
''' Operation Arguments for current operation. '''
|
||||
''' Operation arguments. '''
|
||||
return Argument.objects.filter(operation=self)
|
||||
|
||||
def getQ_as_argument(self) -> QuerySet[Argument]:
|
||||
''' Operation Arguments where the operation is used as an argument. '''
|
||||
return Argument.objects.filter(argument=self)
|
||||
|
||||
def getQ_substitutions(self) -> QuerySet[Substitution]:
|
||||
''' Operation substitutions. '''
|
||||
return Substitution.objects.filter(operation=self)
|
||||
|
||||
def getQ_references(self) -> QuerySet[Reference]:
|
||||
''' Operation references. '''
|
||||
return Reference.objects.filter(target=self)
|
||||
|
||||
def getQ_reference_target(self) -> list['Operation']:
|
||||
''' Operation target for current reference. '''
|
||||
return [x.target for x in Reference.objects.filter(reference=self)]
|
||||
|
||||
def setQ_result(self, result: Optional[LibraryItem]) -> None:
|
||||
''' Set result schema. '''
|
||||
if result == self.result:
|
||||
return
|
||||
self.result = result
|
||||
self.save(update_fields=['result'])
|
||||
for reference in self.getQ_references():
|
||||
reference.reference.result = result
|
||||
reference.reference.save(update_fields=['result'])
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
''' Delete operation. '''
|
||||
for ref in self.getQ_references():
|
||||
ref.reference.delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
|
|
@ -1,25 +1,40 @@
|
|||
''' Models: OSS API. '''
|
||||
# pylint: disable=duplicate-code
|
||||
from typing import Optional, cast
|
||||
|
||||
from cctext import extract_entities
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from apps.library.models import Editor, LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import Constituenta, OrderManager, RSFormCached
|
||||
from apps.rsform.graph import Graph
|
||||
from apps.rsform.models import (
|
||||
DELETED_ALIAS,
|
||||
INSERT_LAST,
|
||||
Constituenta,
|
||||
CstType,
|
||||
RSForm,
|
||||
extract_globals,
|
||||
replace_entities,
|
||||
replace_globals
|
||||
)
|
||||
|
||||
from .Argument import Argument
|
||||
from .Block import Block
|
||||
from .Inheritance import Inheritance
|
||||
from .Layout import Layout
|
||||
from .Operation import Operation, OperationType
|
||||
from .Reference import Reference
|
||||
from .Operation import Operation
|
||||
from .Substitution import Substitution
|
||||
|
||||
CstMapping = dict[str, Optional[Constituenta]]
|
||||
CstSubstitution = list[tuple[Constituenta, Constituenta]]
|
||||
|
||||
|
||||
class OperationSchema:
|
||||
''' Operations schema API wrapper. No caching, propagation and minimal side effects. '''
|
||||
''' Operations schema API. '''
|
||||
|
||||
def __init__(self, model: LibraryItem):
|
||||
self.model = model
|
||||
self.cache = OssCache(self)
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'OperationSchema':
|
||||
|
@ -29,44 +44,101 @@ class OperationSchema:
|
|||
return OperationSchema(model)
|
||||
|
||||
@staticmethod
|
||||
def owned_schemasQ(item: LibraryItem) -> QuerySet[LibraryItem]:
|
||||
''' Get QuerySet containing all result schemas owned by current OSS. '''
|
||||
return LibraryItem.objects.filter(
|
||||
producer__oss=item,
|
||||
owner_id=item.owner_id,
|
||||
location=item.location
|
||||
)
|
||||
def from_id(pk: int) -> 'OperationSchema':
|
||||
''' Get LibraryItem by pk. '''
|
||||
model = LibraryItem.objects.get(pk=pk)
|
||||
return OperationSchema(model)
|
||||
|
||||
@staticmethod
|
||||
def layoutQ(itemID: int) -> Layout:
|
||||
''' OSS layout. '''
|
||||
return Layout.objects.get(oss_id=itemID)
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
''' Save wrapper. '''
|
||||
self.model.save(*args, **kwargs)
|
||||
|
||||
def refresh_from_db(self) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.refresh_from_db()
|
||||
|
||||
def operations(self) -> QuerySet[Operation]:
|
||||
''' Get QuerySet containing all operations of current OSS. '''
|
||||
return Operation.objects.filter(oss=self.model)
|
||||
|
||||
def blocks(self) -> QuerySet[Block]:
|
||||
''' Get QuerySet containing all blocks of current OSS. '''
|
||||
return Block.objects.filter(oss=self.model)
|
||||
|
||||
def arguments(self) -> QuerySet[Argument]:
|
||||
''' Operation arguments. '''
|
||||
return Argument.objects.filter(operation__oss=self.model)
|
||||
|
||||
def layout(self) -> Layout:
|
||||
''' OSS layout. '''
|
||||
result = Layout.objects.filter(oss=self.model).first()
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
def substitutions(self) -> QuerySet[Substitution]:
|
||||
''' Operation substitutions. '''
|
||||
return Substitution.objects.filter(operation__oss=self.model)
|
||||
|
||||
def inheritance(self) -> QuerySet[Inheritance]:
|
||||
''' Operation inheritances. '''
|
||||
return Inheritance.objects.filter(operation__oss=self.model)
|
||||
|
||||
def owned_schemas(self) -> QuerySet[LibraryItem]:
|
||||
''' Get QuerySet containing all result schemas owned by current OSS. '''
|
||||
return LibraryItem.objects.filter(
|
||||
producer__oss=self.model,
|
||||
owner_id=self.model.owner_id,
|
||||
location=self.model.location
|
||||
)
|
||||
|
||||
def update_layout(self, data: dict) -> None:
|
||||
''' Update graphical layout. '''
|
||||
layout = self.layout()
|
||||
layout.data = data
|
||||
layout.save()
|
||||
|
||||
def create_operation(self, **kwargs) -> Operation:
|
||||
''' Create Operation. '''
|
||||
result = Operation.objects.create(oss=self.model, **kwargs)
|
||||
return result
|
||||
|
||||
def create_reference(self, target: Operation) -> Operation:
|
||||
''' Create Reference Operation. '''
|
||||
result = Operation.objects.create(
|
||||
oss=self.model,
|
||||
operation_type=OperationType.REFERENCE,
|
||||
result=target.result,
|
||||
parent=target.parent
|
||||
)
|
||||
Reference.objects.create(reference=result, target=target)
|
||||
self.cache.insert_operation(result)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def create_block(self, **kwargs) -> Block:
|
||||
''' Create Block. '''
|
||||
result = Block.objects.create(oss=self.model, **kwargs)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def delete_operation(self, target: int, keep_constituents: bool = False):
|
||||
''' Delete Operation. '''
|
||||
self.cache.ensure_loaded()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
schema = self.cache.get_schema(operation)
|
||||
children = self.cache.graph.outputs[target]
|
||||
if schema is not None and len(children) > 0:
|
||||
if not keep_constituents:
|
||||
self.before_delete_cst(schema, schema.cache.constituents)
|
||||
else:
|
||||
items = schema.cache.constituents
|
||||
ids = [cst.pk for cst in items]
|
||||
inheritance_to_delete: list[Inheritance] = []
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
self._undo_substitutions_cst(items, child_operation, child_schema)
|
||||
for item in self.cache.inheritance[child_id]:
|
||||
if item.parent_id in ids:
|
||||
inheritance_to_delete.append(item)
|
||||
for item in inheritance_to_delete:
|
||||
self.cache.remove_inheritance(item)
|
||||
Inheritance.objects.filter(pk__in=[item.pk for item in inheritance_to_delete]).delete()
|
||||
self.cache.remove_operation(target)
|
||||
operation.delete()
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def delete_block(self, target: Block):
|
||||
''' Delete Block. '''
|
||||
new_parent = target.parent
|
||||
|
@ -79,10 +151,108 @@ class OperationSchema:
|
|||
operation.parent = new_parent
|
||||
operation.save(update_fields=['parent'])
|
||||
target.delete()
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def create_input(self, operation: Operation) -> RSFormCached:
|
||||
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
|
||||
''' Set input schema for operation. '''
|
||||
operation = self.cache.operation_by_id[target]
|
||||
has_children = len(self.cache.graph.outputs[target]) > 0
|
||||
old_schema = self.cache.get_schema(operation)
|
||||
if schema == old_schema:
|
||||
return
|
||||
|
||||
if old_schema is not None:
|
||||
if has_children:
|
||||
self.before_delete_cst(old_schema, old_schema.cache.constituents)
|
||||
self.cache.remove_schema(old_schema)
|
||||
|
||||
operation.result = schema
|
||||
if schema is not None:
|
||||
operation.alias = schema.alias
|
||||
operation.title = schema.title
|
||||
operation.description = schema.description
|
||||
operation.save(update_fields=['result', 'alias', 'title', 'description'])
|
||||
|
||||
if schema is not None and has_children:
|
||||
rsform = RSForm(schema)
|
||||
self.after_create_cst(rsform, list(rsform.constituents().order_by('order')))
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||
''' Set arguments of target Operation. '''
|
||||
self.cache.ensure_loaded()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
processed: list[Operation] = []
|
||||
updated: list[Argument] = []
|
||||
deleted: list[Argument] = []
|
||||
for current in operation.getQ_arguments():
|
||||
if current.argument not in arguments:
|
||||
deleted.append(current)
|
||||
else:
|
||||
processed.append(current.argument)
|
||||
current.order = arguments.index(current.argument)
|
||||
updated.append(current)
|
||||
if len(deleted) > 0:
|
||||
self.before_delete_arguments(operation, [x.argument for x in deleted])
|
||||
for deleted_arg in deleted:
|
||||
self.cache.remove_argument(deleted_arg)
|
||||
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
|
||||
Argument.objects.bulk_update(updated, ['order'])
|
||||
|
||||
added: list[Operation] = []
|
||||
for order, arg in enumerate(arguments):
|
||||
if arg not in processed:
|
||||
processed.append(arg)
|
||||
new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
|
||||
self.cache.insert_argument(new_arg)
|
||||
added.append(arg)
|
||||
if len(added) > 0:
|
||||
self.after_create_arguments(operation, added)
|
||||
if len(added) > 0 or len(deleted) > 0:
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
||||
''' Clear all arguments for target Operation. '''
|
||||
self.cache.ensure_loaded()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
schema = self.cache.get_schema(operation)
|
||||
processed: list[dict] = []
|
||||
deleted: list[Substitution] = []
|
||||
for current in operation.getQ_substitutions():
|
||||
subs = [
|
||||
x for x in substitutes
|
||||
if x['original'] == current.original and x['substitution'] == current.substitution
|
||||
]
|
||||
if len(subs) == 0:
|
||||
deleted.append(current)
|
||||
else:
|
||||
processed.append(subs[0])
|
||||
if len(deleted) > 0:
|
||||
if schema is not None:
|
||||
for sub in deleted:
|
||||
self._undo_substitution(schema, sub)
|
||||
else:
|
||||
for sub in deleted:
|
||||
self.cache.remove_substitution(sub)
|
||||
Substitution.objects.filter(pk__in=[x.pk for x in deleted]).delete()
|
||||
|
||||
added: list[Substitution] = []
|
||||
for sub_item in substitutes:
|
||||
if sub_item not in processed:
|
||||
new_sub = Substitution.objects.create(
|
||||
operation=operation,
|
||||
original=sub_item['original'],
|
||||
substitution=sub_item['substitution']
|
||||
)
|
||||
added.append(new_sub)
|
||||
self._process_added_substitutions(schema, added)
|
||||
|
||||
if len(added) > 0 or len(deleted) > 0:
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def create_input(self, operation: Operation) -> RSForm:
|
||||
''' Create input RSForm for given Operation. '''
|
||||
schema = RSFormCached.create(
|
||||
schema = RSForm.create(
|
||||
owner=self.model.owner,
|
||||
alias=operation.alias,
|
||||
title=operation.title,
|
||||
|
@ -92,51 +262,28 @@ class OperationSchema:
|
|||
location=self.model.location
|
||||
)
|
||||
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
|
||||
operation.setQ_result(schema.model)
|
||||
operation.result = schema.model
|
||||
operation.save()
|
||||
self.save(update_fields=['time_update'])
|
||||
return schema
|
||||
|
||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||
''' Set arguments of target Operation. '''
|
||||
Argument.objects.filter(operation_id=target).delete()
|
||||
order = 0
|
||||
for arg in arguments:
|
||||
Argument.objects.create(
|
||||
operation_id=target,
|
||||
argument=arg,
|
||||
order=order
|
||||
)
|
||||
order += 1
|
||||
|
||||
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
||||
''' Set Substitutions for target Operation. '''
|
||||
Substitution.objects.filter(operation_id=target).delete()
|
||||
for sub_item in substitutes:
|
||||
Substitution.objects.create(
|
||||
operation_id=target,
|
||||
original=sub_item['original'],
|
||||
substitution=sub_item['substitution']
|
||||
)
|
||||
|
||||
def execute_operation(self, operation: Operation) -> None:
|
||||
def execute_operation(self, operation: Operation) -> bool:
|
||||
''' Execute target Operation. '''
|
||||
schemas: list[int] = [
|
||||
arg.argument.result_id
|
||||
for arg in Argument.objects
|
||||
.filter(operation=operation)
|
||||
.select_related('argument')
|
||||
.only('argument__result_id')
|
||||
.order_by('order')
|
||||
if arg.argument.result_id is not None
|
||||
schemas = [
|
||||
arg.argument.result
|
||||
for arg in operation.getQ_arguments().order_by('order')
|
||||
if arg.argument.result is not None
|
||||
]
|
||||
if len(schemas) == 0:
|
||||
return
|
||||
return False
|
||||
substitutions = operation.getQ_substitutions()
|
||||
receiver = self.create_input(operation)
|
||||
receiver = self.create_input(self.cache.operation_by_id[operation.pk])
|
||||
|
||||
parents: dict = {}
|
||||
children: dict = {}
|
||||
for operand in schemas:
|
||||
items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
|
||||
schema = RSForm(operand)
|
||||
items = list(schema.constituents().order_by('order'))
|
||||
new_items = receiver.insert_copy(items)
|
||||
for (i, cst) in enumerate(new_items):
|
||||
parents[cst.pk] = items[i]
|
||||
|
@ -149,7 +296,7 @@ class OperationSchema:
|
|||
translated_substitutions.append((original, replacement))
|
||||
receiver.substitute(translated_substitutions)
|
||||
|
||||
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
|
||||
for cst in receiver.constituents().order_by('order'):
|
||||
parent = parents.get(cst.pk)
|
||||
assert parent is not None
|
||||
Inheritance.objects.create(
|
||||
|
@ -158,6 +305,646 @@ class OperationSchema:
|
|||
parent=parent
|
||||
)
|
||||
|
||||
OrderManager(receiver).restore_order()
|
||||
receiver.restore_order()
|
||||
receiver.reset_aliases()
|
||||
receiver.resolve_all_text()
|
||||
|
||||
if len(self.cache.graph.outputs[operation.pk]) > 0:
|
||||
self.after_create_cst(receiver, list(receiver.constituents().order_by('order')))
|
||||
self.save(update_fields=['time_update'])
|
||||
return True
|
||||
|
||||
def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]):
|
||||
''' Move list of Constituents to destination Schema inheritor. '''
|
||||
self.cache.ensure_loaded()
|
||||
self.cache.insert_schema(source)
|
||||
self.cache.insert_schema(destination)
|
||||
operation = self.cache.get_operation(destination.model.pk)
|
||||
|
||||
self._undo_substitutions_cst(items, operation, destination)
|
||||
|
||||
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
||||
for item in inheritance_to_delete:
|
||||
self.cache.remove_inheritance(item)
|
||||
Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete()
|
||||
|
||||
def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]:
|
||||
''' Move list of Constituents upstream to destination Schema. '''
|
||||
self.cache.ensure_loaded()
|
||||
self.cache.insert_schema(source)
|
||||
self.cache.insert_schema(destination)
|
||||
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
alias_mapping: dict[str, str] = {}
|
||||
for item in self.cache.inheritance[operation.pk]:
|
||||
if item.parent_id in destination.cache.by_id:
|
||||
source_cst = source.cache.by_id[item.child_id]
|
||||
destination_cst = destination.cache.by_id[item.parent_id]
|
||||
alias_mapping[source_cst.alias] = destination_cst.alias
|
||||
|
||||
new_items = destination.insert_copy(items, initial_mapping=alias_mapping)
|
||||
for index, cst in enumerate(new_items):
|
||||
new_inheritance = Inheritance.objects.create(
|
||||
operation=operation,
|
||||
child=items[index],
|
||||
parent=cst
|
||||
)
|
||||
self.cache.insert_inheritance(new_inheritance)
|
||||
self.after_create_cst(destination, new_items, exclude=[operation.pk])
|
||||
|
||||
return new_items
|
||||
|
||||
def after_create_cst(
|
||||
self, source: RSForm,
|
||||
cst_list: list[Constituenta],
|
||||
exclude: Optional[list[int]] = None
|
||||
) -> None:
|
||||
''' Trigger cascade resolutions when new Constituenta is created. '''
|
||||
self.cache.insert_schema(source)
|
||||
inserted_aliases = [cst.alias for cst in cst_list]
|
||||
depend_aliases: set[str] = set()
|
||||
for new_cst in cst_list:
|
||||
depend_aliases.update(new_cst.extract_references())
|
||||
depend_aliases.difference_update(inserted_aliases)
|
||||
alias_mapping: CstMapping = {}
|
||||
for alias in depend_aliases:
|
||||
cst = source.cache.by_alias.get(alias)
|
||||
if cst is not None:
|
||||
alias_mapping[alias] = cst
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
||||
|
||||
def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_change_cst_type(operation.pk, target.pk, cast(CstType, target.cst_type))
|
||||
|
||||
def after_update_cst(self, source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
depend_aliases = self._extract_data_references(data, old_data)
|
||||
alias_mapping: CstMapping = {}
|
||||
for alias in depend_aliases:
|
||||
cst = source.cache.by_alias.get(alias)
|
||||
if cst is not None:
|
||||
alias_mapping[alias] = cst
|
||||
self._cascade_update_cst(
|
||||
operation=operation.pk,
|
||||
cst_id=target.pk,
|
||||
data=data,
|
||||
old_data=old_data,
|
||||
mapping=alias_mapping
|
||||
)
|
||||
|
||||
def before_delete_cst(self, source: RSForm, target: list[Constituenta]) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are deleted. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_delete_inherited(operation.pk, target)
|
||||
|
||||
def before_substitute(self, source: RSForm, substitutions: CstSubstitution) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are substituted. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_before_substitute(substitutions, operation)
|
||||
|
||||
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||
''' Trigger cascade resolutions before arguments are deleted. '''
|
||||
if target.result_id is None:
|
||||
return
|
||||
for argument in arguments:
|
||||
parent_schema = self.cache.get_schema(argument)
|
||||
if parent_schema is not None:
|
||||
self._execute_delete_inherited(target.pk, parent_schema.cache.constituents)
|
||||
|
||||
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||
''' Trigger cascade resolutions after arguments are created. '''
|
||||
schema = self.cache.get_schema(target)
|
||||
if schema is None:
|
||||
return
|
||||
for argument in arguments:
|
||||
parent_schema = self.cache.get_schema(argument)
|
||||
if parent_schema is None:
|
||||
continue
|
||||
self._execute_inherit_cst(
|
||||
target_operation=target.pk,
|
||||
source=parent_schema,
|
||||
items=list(parent_schema.constituents().order_by('order')),
|
||||
mapping={}
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
def _cascade_inherit_cst(
|
||||
self, target_operation: int,
|
||||
source: RSForm,
|
||||
items: list[Constituenta],
|
||||
mapping: CstMapping,
|
||||
exclude: Optional[list[int]] = None
|
||||
) -> None:
|
||||
children = self.cache.graph.outputs[target_operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
for child_id in children:
|
||||
if not exclude or child_id not in exclude:
|
||||
self._execute_inherit_cst(child_id, source, items, mapping)
|
||||
|
||||
def _execute_inherit_cst(
|
||||
self,
|
||||
target_operation: int,
|
||||
source: RSForm,
|
||||
items: list[Constituenta],
|
||||
mapping: CstMapping
|
||||
) -> None:
|
||||
operation = self.cache.operation_by_id[target_operation]
|
||||
destination = self.cache.get_schema(operation)
|
||||
if destination is None:
|
||||
return
|
||||
|
||||
self.cache.ensure_loaded()
|
||||
new_mapping = self._transform_mapping(mapping, operation, destination)
|
||||
alias_mapping = OperationSchema._produce_alias_mapping(new_mapping)
|
||||
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
||||
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
||||
for index, cst in enumerate(new_cst_list):
|
||||
new_inheritance = Inheritance.objects.create(
|
||||
operation=operation,
|
||||
child=cst,
|
||||
parent=items[index]
|
||||
)
|
||||
self.cache.insert_inheritance(new_inheritance)
|
||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||
self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
|
||||
|
||||
def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
||||
children = self.cache.graph.outputs[operation_id]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||
if successor_id is None:
|
||||
continue
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
if child_schema.change_cst_type(successor_id, ctype):
|
||||
self._cascade_change_cst_type(child_id, successor_id, ctype)
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
def _cascade_update_cst(
|
||||
self,
|
||||
operation: int,
|
||||
cst_id: int,
|
||||
data: dict, old_data: dict,
|
||||
mapping: CstMapping
|
||||
) -> None:
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||
if successor_id is None:
|
||||
continue
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
assert child_schema is not None
|
||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||
alias_mapping = OperationSchema._produce_alias_mapping(new_mapping)
|
||||
successor = child_schema.cache.by_id.get(successor_id)
|
||||
if successor is None:
|
||||
continue
|
||||
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
|
||||
if len(new_data) == 0:
|
||||
continue
|
||||
new_old_data = child_schema.update_cst(successor, new_data)
|
||||
if len(new_old_data) == 0:
|
||||
continue
|
||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||
self._cascade_update_cst(
|
||||
operation=child_id,
|
||||
cst_id=successor_id,
|
||||
data=new_data,
|
||||
old_data=new_old_data,
|
||||
mapping=new_mapping
|
||||
)
|
||||
|
||||
def _cascade_delete_inherited(self, operation: int, target: list[Constituenta]) -> None:
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
for child_id in children:
|
||||
self._execute_delete_inherited(child_id, target)
|
||||
|
||||
def _execute_delete_inherited(self, operation_id: int, parent_cst: list[Constituenta]) -> None:
|
||||
operation = self.cache.operation_by_id[operation_id]
|
||||
schema = self.cache.get_schema(operation)
|
||||
if schema is None:
|
||||
return
|
||||
self._undo_substitutions_cst(parent_cst, operation, schema)
|
||||
target_ids = self.cache.get_inheritors_list([cst.pk for cst in parent_cst], operation_id)
|
||||
target_cst = [schema.cache.by_id[cst_id] for cst_id in target_ids]
|
||||
self._cascade_delete_inherited(operation_id, target_cst)
|
||||
if len(target_cst) > 0:
|
||||
self.cache.remove_cst(operation_id, target_ids)
|
||||
schema.delete_cst(target_cst)
|
||||
|
||||
def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
|
||||
children = self.cache.graph.outputs[operation.pk]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
||||
if len(new_substitutions) == 0:
|
||||
continue
|
||||
self._cascade_before_substitute(new_substitutions, child_operation)
|
||||
child_schema.substitute(new_substitutions)
|
||||
|
||||
def _cascade_partial_mapping(
|
||||
self,
|
||||
mapping: CstMapping,
|
||||
target: list[int],
|
||||
operation: int,
|
||||
schema: RSForm
|
||||
) -> None:
|
||||
alias_mapping = OperationSchema._produce_alias_mapping(mapping)
|
||||
schema.apply_partial_mapping(alias_mapping, target)
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||
if not new_mapping:
|
||||
continue
|
||||
new_target = self.cache.get_inheritors_list(target, child_id)
|
||||
if len(new_target) == 0:
|
||||
continue
|
||||
self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema)
|
||||
|
||||
@staticmethod
|
||||
def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for alias, cst in mapping.items():
|
||||
if cst is None:
|
||||
result[alias] = DELETED_ALIAS
|
||||
else:
|
||||
result[alias] = cst.alias
|
||||
return result
|
||||
|
||||
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSForm) -> CstMapping:
|
||||
if len(mapping) == 0:
|
||||
return mapping
|
||||
result: CstMapping = {}
|
||||
for alias, cst in mapping.items():
|
||||
if cst is None:
|
||||
result[alias] = None
|
||||
continue
|
||||
successor_id = self.cache.get_successor(cst.pk, operation.pk)
|
||||
if successor_id is None:
|
||||
continue
|
||||
successor = schema.cache.by_id.get(successor_id)
|
||||
if successor is None:
|
||||
continue
|
||||
result[alias] = successor
|
||||
return result
|
||||
|
||||
def _determine_insert_position(
|
||||
self, prototype_id: int,
|
||||
operation: Operation,
|
||||
source: RSForm,
|
||||
destination: RSForm
|
||||
) -> int:
|
||||
''' Determine insert_after for new constituenta. '''
|
||||
prototype = source.cache.by_id[prototype_id]
|
||||
prototype_index = source.cache.constituents.index(prototype)
|
||||
if prototype_index == 0:
|
||||
return 0
|
||||
prev_cst = source.cache.constituents[prototype_index - 1]
|
||||
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
||||
if inherited_prev_id is None:
|
||||
return INSERT_LAST
|
||||
prev_cst = destination.cache.by_id[inherited_prev_id]
|
||||
prev_index = destination.cache.constituents.index(prev_cst)
|
||||
return prev_index + 1
|
||||
|
||||
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
|
||||
result: set[str] = set()
|
||||
if 'definition_formal' in data:
|
||||
result.update(extract_globals(data['definition_formal']))
|
||||
result.update(extract_globals(old_data['definition_formal']))
|
||||
if 'term_raw' in data:
|
||||
result.update(extract_entities(data['term_raw']))
|
||||
result.update(extract_entities(old_data['term_raw']))
|
||||
if 'definition_raw' in data:
|
||||
result.update(extract_entities(data['definition_raw']))
|
||||
result.update(extract_entities(old_data['definition_raw']))
|
||||
return result
|
||||
|
||||
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
|
||||
new_data = {}
|
||||
if 'term_forms' in data:
|
||||
if old_data['term_forms'] == cst.term_forms:
|
||||
new_data['term_forms'] = data['term_forms']
|
||||
if 'convention' in data:
|
||||
new_data['convention'] = data['convention']
|
||||
if 'definition_formal' in data:
|
||||
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
|
||||
if 'term_raw' in data:
|
||||
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
|
||||
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
|
||||
if 'definition_raw' in data:
|
||||
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
|
||||
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
|
||||
return new_data
|
||||
|
||||
def _transform_substitutions(
|
||||
self,
|
||||
target: CstSubstitution,
|
||||
operation: int,
|
||||
schema: RSForm
|
||||
) -> CstSubstitution:
|
||||
result: CstSubstitution = []
|
||||
for current_sub in target:
|
||||
sub_replaced = False
|
||||
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation)
|
||||
if new_substitution_id is None:
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.original_id == current_sub[1].pk:
|
||||
sub_replaced = True
|
||||
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation)
|
||||
break
|
||||
|
||||
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation)
|
||||
original_replaced = False
|
||||
if new_original_id is None:
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.original_id == current_sub[0].pk:
|
||||
original_replaced = True
|
||||
sub.original_id = current_sub[1].pk
|
||||
sub.save()
|
||||
new_original_id = new_substitution_id
|
||||
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation)
|
||||
break
|
||||
|
||||
if sub_replaced and original_replaced:
|
||||
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
|
||||
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.substitution_id == current_sub[0].pk:
|
||||
sub.substitution_id = current_sub[1].pk
|
||||
sub.save()
|
||||
|
||||
if new_original_id is not None and new_substitution_id is not None:
|
||||
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
|
||||
return result
|
||||
|
||||
def _undo_substitutions_cst(self, target: list[Constituenta], operation: Operation, schema: RSForm) -> None:
|
||||
target_ids = [cst.pk for cst in target]
|
||||
to_process = []
|
||||
for sub in self.cache.substitutions[operation.pk]:
|
||||
if sub.original_id in target_ids or sub.substitution_id in target_ids:
|
||||
to_process.append(sub)
|
||||
for sub in to_process:
|
||||
self._undo_substitution(schema, sub, target_ids)
|
||||
|
||||
def _undo_substitution(
|
||||
self,
|
||||
schema: RSForm,
|
||||
target: Substitution,
|
||||
ignore_parents: Optional[list[int]] = None
|
||||
) -> None:
|
||||
if ignore_parents is None:
|
||||
ignore_parents = []
|
||||
operation_id = target.operation_id
|
||||
original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target)
|
||||
|
||||
dependant = []
|
||||
for cst_id in original_schema.get_dependant([original_cst.pk]):
|
||||
if cst_id not in ignore_parents:
|
||||
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
||||
if inheritor_id is not None:
|
||||
dependant.append(inheritor_id)
|
||||
|
||||
self.cache.substitutions[operation_id].remove(target)
|
||||
target.delete()
|
||||
|
||||
new_original: Optional[Constituenta] = None
|
||||
if original_cst.pk not in ignore_parents:
|
||||
full_cst = Constituenta.objects.get(pk=original_cst.pk)
|
||||
self.after_create_cst(original_schema, [full_cst])
|
||||
new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id)
|
||||
assert new_original_id is not None
|
||||
new_original = schema.cache.by_id[new_original_id]
|
||||
if len(dependant) == 0:
|
||||
return
|
||||
|
||||
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id)
|
||||
assert substitution_id is not None
|
||||
substitution_inheritor = schema.cache.by_id[substitution_id]
|
||||
mapping = {substitution_inheritor.alias: new_original}
|
||||
self._cascade_partial_mapping(mapping, dependant, operation_id, schema)
|
||||
|
||||
def _process_added_substitutions(self, schema: Optional[RSForm], added: list[Substitution]) -> None:
|
||||
if len(added) == 0:
|
||||
return
|
||||
if schema is None:
|
||||
for sub in added:
|
||||
self.cache.insert_substitution(sub)
|
||||
return
|
||||
|
||||
cst_mapping: CstSubstitution = []
|
||||
for sub in added:
|
||||
original_id = self.cache.get_inheritor(sub.original_id, sub.operation_id)
|
||||
substitution_id = self.cache.get_inheritor(sub.substitution_id, sub.operation_id)
|
||||
if original_id is None or substitution_id is None:
|
||||
raise ValueError('Substitutions not found.')
|
||||
original_cst = schema.cache.by_id[original_id]
|
||||
substitution_cst = schema.cache.by_id[substitution_id]
|
||||
cst_mapping.append((original_cst, substitution_cst))
|
||||
self.before_substitute(schema, cst_mapping)
|
||||
schema.substitute(cst_mapping)
|
||||
for sub in added:
|
||||
self.cache.insert_substitution(sub)
|
||||
|
||||
|
||||
class OssCache:
|
||||
''' Cache for OSS data. '''
|
||||
|
||||
def __init__(self, oss: OperationSchema):
|
||||
self._oss = oss
|
||||
self._schemas: list[RSForm] = []
|
||||
self._schema_by_id: dict[int, RSForm] = {}
|
||||
|
||||
self.operations = list(oss.operations().only('result_id'))
|
||||
self.operation_by_id = {operation.pk: operation for operation in self.operations}
|
||||
self.graph = Graph[int]()
|
||||
for operation in self.operations:
|
||||
self.graph.add_node(operation.pk)
|
||||
for argument in self._oss.arguments().only('operation_id', 'argument_id').order_by('order'):
|
||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||
|
||||
self.is_loaded = False
|
||||
self.substitutions: dict[int, list[Substitution]] = {}
|
||||
self.inheritance: dict[int, list[Inheritance]] = {}
|
||||
|
||||
def ensure_loaded(self) -> None:
|
||||
''' Ensure cache is fully loaded. '''
|
||||
if self.is_loaded:
|
||||
return
|
||||
self.is_loaded = True
|
||||
for operation in self.operations:
|
||||
self.inheritance[operation.pk] = []
|
||||
self.substitutions[operation.pk] = []
|
||||
for sub in self._oss.substitutions().only('operation_id', 'original_id', 'substitution_id'):
|
||||
self.substitutions[sub.operation_id].append(sub)
|
||||
for item in self._oss.inheritance().only('operation_id', 'parent_id', 'child_id'):
|
||||
self.inheritance[item.operation_id].append(item)
|
||||
|
||||
def get_schema(self, operation: Operation) -> Optional[RSForm]:
|
||||
''' Get schema by Operation. '''
|
||||
if operation.result_id is None:
|
||||
return None
|
||||
if operation.result_id in self._schema_by_id:
|
||||
return self._schema_by_id[operation.result_id]
|
||||
else:
|
||||
schema = RSForm.from_id(operation.result_id)
|
||||
schema.cache.ensure_loaded()
|
||||
self._insert_new(schema)
|
||||
return schema
|
||||
|
||||
def get_operation(self, schema: int) -> Operation:
|
||||
''' Get operation by schema. '''
|
||||
for operation in self.operations:
|
||||
if operation.result_id == schema:
|
||||
return operation
|
||||
raise ValueError(f'Operation for schema {schema} not found')
|
||||
|
||||
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||
''' Get child for parent inside target RSFrom. '''
|
||||
for item in self.inheritance[operation]:
|
||||
if item.parent_id == parent_cst:
|
||||
return item.child_id
|
||||
return None
|
||||
|
||||
def get_inheritors_list(self, target: list[int], operation: int) -> list[int]:
|
||||
''' Get child for parent inside target RSFrom. '''
|
||||
result = []
|
||||
for item in self.inheritance[operation]:
|
||||
if item.parent_id in target:
|
||||
result.append(item.child_id)
|
||||
return result
|
||||
|
||||
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||
''' Get child for parent inside target RSFrom including substitutions. '''
|
||||
for sub in self.substitutions[operation]:
|
||||
if sub.original_id == parent_cst:
|
||||
return self.get_inheritor(sub.substitution_id, operation)
|
||||
return self.get_inheritor(parent_cst, operation)
|
||||
|
||||
def insert_schema(self, schema: RSForm) -> None:
|
||||
''' Insert new schema. '''
|
||||
if not self._schema_by_id.get(schema.model.pk):
|
||||
schema.cache.ensure_loaded()
|
||||
self._insert_new(schema)
|
||||
|
||||
def insert_operation(self, operation: Operation) -> None:
|
||||
''' Insert new operation. '''
|
||||
self.operations.append(operation)
|
||||
self.operation_by_id[operation.pk] = operation
|
||||
self.graph.add_node(operation.pk)
|
||||
if self.is_loaded:
|
||||
self.substitutions[operation.pk] = []
|
||||
self.inheritance[operation.pk] = []
|
||||
|
||||
def insert_argument(self, argument: Argument) -> None:
|
||||
''' Insert new argument. '''
|
||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||
|
||||
def insert_inheritance(self, inheritance: Inheritance) -> None:
|
||||
''' Insert new inheritance. '''
|
||||
self.inheritance[inheritance.operation_id].append(inheritance)
|
||||
|
||||
def insert_substitution(self, sub: Substitution) -> None:
|
||||
''' Insert new substitution. '''
|
||||
self.substitutions[sub.operation_id].append(sub)
|
||||
|
||||
def remove_cst(self, operation: int, target: list[int]) -> None:
|
||||
''' Remove constituents from operation. '''
|
||||
subs_to_delete = [
|
||||
sub for sub in self.substitutions[operation]
|
||||
if sub.original_id in target or sub.substitution_id in target
|
||||
]
|
||||
for sub in subs_to_delete:
|
||||
self.substitutions[operation].remove(sub)
|
||||
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
|
||||
for item in inherit_to_delete:
|
||||
self.inheritance[operation].remove(item)
|
||||
|
||||
def remove_schema(self, schema: RSForm) -> None:
|
||||
''' Remove schema from cache. '''
|
||||
self._schemas.remove(schema)
|
||||
del self._schema_by_id[schema.model.pk]
|
||||
|
||||
def remove_operation(self, operation: int) -> None:
|
||||
''' Remove operation from cache. '''
|
||||
target = self.operation_by_id[operation]
|
||||
self.graph.remove_node(operation)
|
||||
if target.result_id in self._schema_by_id:
|
||||
self._schemas.remove(self._schema_by_id[target.result_id])
|
||||
del self._schema_by_id[target.result_id]
|
||||
self.operations.remove(self.operation_by_id[operation])
|
||||
del self.operation_by_id[operation]
|
||||
if self.is_loaded:
|
||||
del self.substitutions[operation]
|
||||
del self.inheritance[operation]
|
||||
|
||||
def remove_argument(self, argument: Argument) -> None:
|
||||
''' Remove argument from cache. '''
|
||||
self.graph.remove_edge(argument.argument_id, argument.operation_id)
|
||||
|
||||
def remove_substitution(self, target: Substitution) -> None:
|
||||
''' Remove substitution from cache. '''
|
||||
self.substitutions[target.operation_id].remove(target)
|
||||
|
||||
def remove_inheritance(self, target: Inheritance) -> None:
|
||||
''' Remove inheritance from cache. '''
|
||||
self.inheritance[target.operation_id].remove(target)
|
||||
|
||||
def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]:
|
||||
operation = self.operation_by_id[sub.operation_id]
|
||||
parents = self.graph.inputs[operation.pk]
|
||||
original_cst = None
|
||||
substitution_cst = None
|
||||
original_schema = None
|
||||
substitution_schema = None
|
||||
for parent_id in parents:
|
||||
parent_schema = self.get_schema(self.operation_by_id[parent_id])
|
||||
if parent_schema is None:
|
||||
continue
|
||||
if sub.original_id in parent_schema.cache.by_id:
|
||||
original_schema = parent_schema
|
||||
original_cst = original_schema.cache.by_id[sub.original_id]
|
||||
if sub.substitution_id in parent_schema.cache.by_id:
|
||||
substitution_schema = parent_schema
|
||||
substitution_cst = substitution_schema.cache.by_id[sub.substitution_id]
|
||||
if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None:
|
||||
raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.')
|
||||
return original_schema, substitution_schema, original_cst, substitution_cst
|
||||
|
||||
def _insert_new(self, schema: RSForm) -> None:
|
||||
self._schemas.append(schema)
|
||||
self._schema_by_id[schema.model.pk] = schema
|
||||
|
|
|
@ -1,881 +0,0 @@
|
|||
''' Models: OSS API. '''
|
||||
# pylint: disable=duplicate-code
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from cctext import extract_entities
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from apps.library.models import Editor, LibraryItem
|
||||
from apps.rsform.graph import Graph
|
||||
from apps.rsform.models import (
|
||||
DELETED_ALIAS,
|
||||
INSERT_LAST,
|
||||
Constituenta,
|
||||
CstType,
|
||||
OrderManager,
|
||||
RSFormCached,
|
||||
extract_globals,
|
||||
replace_entities,
|
||||
replace_globals
|
||||
)
|
||||
|
||||
from .Argument import Argument
|
||||
from .Inheritance import Inheritance
|
||||
from .Operation import Operation, OperationType
|
||||
from .Reference import Reference
|
||||
from .Substitution import Substitution
|
||||
|
||||
CstMapping = dict[str, Optional[Constituenta]]
|
||||
CstSubstitution = list[tuple[Constituenta, Constituenta]]
|
||||
|
||||
|
||||
class OperationSchemaCached:
|
||||
''' Operations schema API with caching. '''
|
||||
|
||||
def __init__(self, model: LibraryItem):
|
||||
self.model = model
|
||||
self.cache = OssCache(self)
|
||||
|
||||
def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
||||
''' Delete Reference Operation. '''
|
||||
if not keep_connections:
|
||||
self.delete_operation(target, keep_constituents)
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
reference_target = self.cache.reference_target.get(target)
|
||||
if reference_target:
|
||||
for arg in operation.getQ_as_argument():
|
||||
arg.argument_id = reference_target
|
||||
arg.save()
|
||||
self.cache.remove_operation(target)
|
||||
operation.delete()
|
||||
|
||||
|
||||
def delete_operation(self, target: int, keep_constituents: bool = False):
|
||||
''' Delete Operation. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
children = self.cache.graph.outputs[target]
|
||||
if operation.result is not None and len(children) > 0:
|
||||
ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
|
||||
if not keep_constituents:
|
||||
self._cascade_delete_inherited(operation.pk, ids)
|
||||
else:
|
||||
inheritance_to_delete: list[Inheritance] = []
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
self._undo_substitutions_cst(ids, child_operation, child_schema)
|
||||
for item in self.cache.inheritance[child_id]:
|
||||
if item.parent_id in ids:
|
||||
inheritance_to_delete.append(item)
|
||||
for item in inheritance_to_delete:
|
||||
self.cache.remove_inheritance(item)
|
||||
Inheritance.objects.filter(pk__in=[item.pk for item in inheritance_to_delete]).delete()
|
||||
self.cache.remove_operation(target)
|
||||
operation.delete()
|
||||
|
||||
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
|
||||
''' Set input schema for operation. '''
|
||||
operation = self.cache.operation_by_id[target]
|
||||
has_children = len(self.cache.graph.outputs[target]) > 0
|
||||
old_schema = self.cache.get_schema(operation)
|
||||
if schema is None and old_schema is None or \
|
||||
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk):
|
||||
return
|
||||
|
||||
if old_schema is not None:
|
||||
if has_children:
|
||||
self.before_delete_cst(old_schema.model.pk, [cst.pk for cst in old_schema.cache.constituents])
|
||||
self.cache.remove_schema(old_schema)
|
||||
|
||||
operation.setQ_result(schema)
|
||||
if schema is not None:
|
||||
operation.alias = schema.alias
|
||||
operation.title = schema.title
|
||||
operation.description = schema.description
|
||||
operation.save(update_fields=['alias', 'title', 'description'])
|
||||
|
||||
if schema is not None and has_children:
|
||||
rsform = RSFormCached(schema)
|
||||
self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order')))
|
||||
|
||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||
''' Set arguments of target Operation. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
processed: list[Operation] = []
|
||||
updated: list[Argument] = []
|
||||
deleted: list[Argument] = []
|
||||
for current in operation.getQ_arguments():
|
||||
if current.argument not in arguments:
|
||||
deleted.append(current)
|
||||
else:
|
||||
processed.append(current.argument)
|
||||
current.order = arguments.index(current.argument)
|
||||
updated.append(current)
|
||||
if len(deleted) > 0:
|
||||
self.before_delete_arguments(operation, [x.argument for x in deleted])
|
||||
for deleted_arg in deleted:
|
||||
self.cache.remove_argument(deleted_arg)
|
||||
Argument.objects.filter(pk__in=[x.pk for x in deleted]).delete()
|
||||
Argument.objects.bulk_update(updated, ['order'])
|
||||
|
||||
added: list[Operation] = []
|
||||
for order, arg in enumerate(arguments):
|
||||
if arg not in processed:
|
||||
processed.append(arg)
|
||||
new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
|
||||
self.cache.insert_argument(new_arg)
|
||||
added.append(arg)
|
||||
if len(added) > 0:
|
||||
self.after_create_arguments(operation, added)
|
||||
|
||||
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
|
||||
''' Clear all arguments for target Operation. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
operation = self.cache.operation_by_id[target]
|
||||
schema = self.cache.get_schema(operation)
|
||||
processed: list[dict] = []
|
||||
deleted: list[Substitution] = []
|
||||
for current in operation.getQ_substitutions():
|
||||
subs = [
|
||||
x for x in substitutes
|
||||
if x['original'] == current.original and x['substitution'] == current.substitution
|
||||
]
|
||||
if len(subs) == 0:
|
||||
deleted.append(current)
|
||||
else:
|
||||
processed.append(subs[0])
|
||||
if len(deleted) > 0:
|
||||
if schema is not None:
|
||||
for sub in deleted:
|
||||
self._undo_substitution(schema, sub)
|
||||
else:
|
||||
for sub in deleted:
|
||||
self.cache.remove_substitution(sub)
|
||||
Substitution.objects.filter(pk__in=[x.pk for x in deleted]).delete()
|
||||
|
||||
added: list[Substitution] = []
|
||||
for sub_item in substitutes:
|
||||
if sub_item not in processed:
|
||||
new_sub = Substitution.objects.create(
|
||||
operation=operation,
|
||||
original=sub_item['original'],
|
||||
substitution=sub_item['substitution']
|
||||
)
|
||||
added.append(new_sub)
|
||||
self._process_added_substitutions(schema, added)
|
||||
|
||||
def _create_input(self, operation: Operation) -> RSFormCached:
|
||||
''' Create input RSForm for given Operation. '''
|
||||
schema = RSFormCached.create(
|
||||
owner=self.model.owner,
|
||||
alias=operation.alias,
|
||||
title=operation.title,
|
||||
description=operation.description,
|
||||
visible=False,
|
||||
access_policy=self.model.access_policy,
|
||||
location=self.model.location
|
||||
)
|
||||
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
|
||||
operation.setQ_result(schema.model)
|
||||
return schema
|
||||
|
||||
def execute_operation(self, operation: Operation) -> bool:
|
||||
''' Execute target Operation. '''
|
||||
schemas: list[int] = [
|
||||
arg.argument.result_id
|
||||
for arg in Argument.objects
|
||||
.filter(operation=operation)
|
||||
.select_related('argument')
|
||||
.only('argument__result_id')
|
||||
.order_by('order')
|
||||
if arg.argument.result_id is not None
|
||||
]
|
||||
if len(schemas) == 0:
|
||||
return False
|
||||
substitutions = operation.getQ_substitutions()
|
||||
receiver = self._create_input(self.cache.operation_by_id[operation.pk])
|
||||
|
||||
parents: dict = {}
|
||||
children: dict = {}
|
||||
for operand in schemas:
|
||||
items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
|
||||
new_items = receiver.insert_copy(items)
|
||||
for (i, cst) in enumerate(new_items):
|
||||
parents[cst.pk] = items[i]
|
||||
children[items[i].pk] = cst
|
||||
|
||||
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
|
||||
for sub in substitutions:
|
||||
original = children[sub.original.pk]
|
||||
replacement = children[sub.substitution.pk]
|
||||
translated_substitutions.append((original, replacement))
|
||||
receiver.substitute(translated_substitutions)
|
||||
|
||||
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
|
||||
parent = parents.get(cst.pk)
|
||||
assert parent is not None
|
||||
Inheritance.objects.create(
|
||||
operation_id=operation.pk,
|
||||
child=cst,
|
||||
parent=parent
|
||||
)
|
||||
|
||||
OrderManager(receiver).restore_order()
|
||||
receiver.reset_aliases()
|
||||
receiver.resolve_all_text()
|
||||
|
||||
if len(self.cache.graph.outputs[operation.pk]) > 0:
|
||||
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order'))
|
||||
self.after_create_cst(receiver, receiver_items)
|
||||
receiver.model.save(update_fields=['time_update'])
|
||||
return True
|
||||
|
||||
def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]):
|
||||
''' Move list of Constituents to destination Schema inheritor. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
self.cache.insert_schema(source)
|
||||
self.cache.insert_schema(destination)
|
||||
operation = self.cache.get_operation(destination.model.pk)
|
||||
self._undo_substitutions_cst(items, operation, destination)
|
||||
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
||||
for item in inheritance_to_delete:
|
||||
self.cache.remove_inheritance(item)
|
||||
Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete()
|
||||
|
||||
def relocate_up(self, source: RSFormCached, destination: RSFormCached,
|
||||
items: list[Constituenta]) -> list[Constituenta]:
|
||||
''' Move list of Constituents upstream to destination Schema. '''
|
||||
self.cache.ensure_loaded_subs()
|
||||
self.cache.insert_schema(source)
|
||||
self.cache.insert_schema(destination)
|
||||
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
alias_mapping: dict[str, str] = {}
|
||||
for item in self.cache.inheritance[operation.pk]:
|
||||
if item.parent_id in destination.cache.by_id:
|
||||
source_cst = source.cache.by_id[item.child_id]
|
||||
destination_cst = destination.cache.by_id[item.parent_id]
|
||||
alias_mapping[source_cst.alias] = destination_cst.alias
|
||||
|
||||
new_items = destination.insert_copy(items, initial_mapping=alias_mapping)
|
||||
for index, cst in enumerate(new_items):
|
||||
new_inheritance = Inheritance.objects.create(
|
||||
operation=operation,
|
||||
child=items[index],
|
||||
parent=cst
|
||||
)
|
||||
self.cache.insert_inheritance(new_inheritance)
|
||||
self.after_create_cst(destination, new_items, exclude=[operation.pk])
|
||||
destination.model.save(update_fields=['time_update'])
|
||||
return new_items
|
||||
|
||||
def after_create_cst(
|
||||
self, source: RSFormCached,
|
||||
cst_list: list[Constituenta],
|
||||
exclude: Optional[list[int]] = None
|
||||
) -> None:
|
||||
''' Trigger cascade resolutions when new Constituenta is created. '''
|
||||
source.cache.ensure_loaded()
|
||||
self.cache.insert_schema(source)
|
||||
inserted_aliases = [cst.alias for cst in cst_list]
|
||||
depend_aliases: set[str] = set()
|
||||
for new_cst in cst_list:
|
||||
depend_aliases.update(new_cst.extract_references())
|
||||
depend_aliases.difference_update(inserted_aliases)
|
||||
alias_mapping: CstMapping = {}
|
||||
for alias in depend_aliases:
|
||||
cst = source.cache.by_alias.get(alias)
|
||||
if cst is not None:
|
||||
alias_mapping[alias] = cst
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
||||
|
||||
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
||||
operation = self.cache.get_operation(schemaID)
|
||||
self._cascade_change_cst_type(operation.pk, target, new_type)
|
||||
|
||||
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None:
|
||||
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
||||
self.cache.insert_schema(source)
|
||||
operation = self.cache.get_operation(source.model.pk)
|
||||
depend_aliases = self._extract_data_references(data, old_data)
|
||||
alias_mapping: CstMapping = {}
|
||||
for alias in depend_aliases:
|
||||
cst = source.cache.by_alias.get(alias)
|
||||
if cst is not None:
|
||||
alias_mapping[alias] = cst
|
||||
self._cascade_update_cst(
|
||||
operation=operation.pk,
|
||||
cst_id=target,
|
||||
data=data,
|
||||
old_data=old_data,
|
||||
mapping=alias_mapping
|
||||
)
|
||||
|
||||
def before_delete_cst(self, sourceID: int, target: list[int]) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are deleted. '''
|
||||
operation = self.cache.get_operation(sourceID)
|
||||
self._cascade_delete_inherited(operation.pk, target)
|
||||
|
||||
def before_substitute(self, schemaID: int, substitutions: CstSubstitution) -> None:
|
||||
''' Trigger cascade resolutions before Constituents are substituted. '''
|
||||
operation = self.cache.get_operation(schemaID)
|
||||
self._cascade_before_substitute(substitutions, operation)
|
||||
|
||||
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||
''' Trigger cascade resolutions before arguments are deleted. '''
|
||||
if target.result_id is None:
|
||||
return
|
||||
for argument in arguments:
|
||||
parent_schema = self.cache.get_schema(argument)
|
||||
if parent_schema is not None:
|
||||
self._execute_delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
|
||||
|
||||
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||
''' Trigger cascade resolutions after arguments are created. '''
|
||||
schema = self.cache.get_schema(target)
|
||||
if schema is None:
|
||||
return
|
||||
for argument in arguments:
|
||||
parent_schema = self.cache.get_schema(argument)
|
||||
if parent_schema is None:
|
||||
continue
|
||||
self._execute_inherit_cst(
|
||||
target_operation=target.pk,
|
||||
source=parent_schema,
|
||||
items=list(parent_schema.constituentsQ().order_by('order')),
|
||||
mapping={}
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
def _cascade_inherit_cst(
|
||||
self, target_operation: int,
|
||||
source: RSFormCached,
|
||||
items: list[Constituenta],
|
||||
mapping: CstMapping,
|
||||
exclude: Optional[list[int]] = None
|
||||
) -> None:
|
||||
children = self.cache.graph.outputs[target_operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
for child_id in children:
|
||||
if not exclude or child_id not in exclude:
|
||||
self._execute_inherit_cst(child_id, source, items, mapping)
|
||||
|
||||
def _execute_inherit_cst(
|
||||
self,
|
||||
target_operation: int,
|
||||
source: RSFormCached,
|
||||
items: list[Constituenta],
|
||||
mapping: CstMapping
|
||||
) -> None:
|
||||
operation = self.cache.operation_by_id[target_operation]
|
||||
destination = self.cache.get_schema(operation)
|
||||
if destination is None:
|
||||
return
|
||||
|
||||
self.cache.ensure_loaded_subs()
|
||||
new_mapping = self._transform_mapping(mapping, operation, destination)
|
||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping)
|
||||
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
||||
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
||||
for index, cst in enumerate(new_cst_list):
|
||||
new_inheritance = Inheritance.objects.create(
|
||||
operation=operation,
|
||||
child=cst,
|
||||
parent=items[index]
|
||||
)
|
||||
self.cache.insert_inheritance(new_inheritance)
|
||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||
self._cascade_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
|
||||
|
||||
def _cascade_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
||||
children = self.cache.graph.outputs[operation_id]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||
if successor_id is None:
|
||||
continue
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
if child_schema.change_cst_type(successor_id, ctype):
|
||||
self._cascade_change_cst_type(child_id, successor_id, ctype)
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||
def _cascade_update_cst(
|
||||
self,
|
||||
operation: int,
|
||||
cst_id: int,
|
||||
data: dict, old_data: dict,
|
||||
mapping: CstMapping
|
||||
) -> None:
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||
if successor_id is None:
|
||||
continue
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
assert child_schema is not None
|
||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(new_mapping)
|
||||
successor = child_schema.cache.by_id.get(successor_id)
|
||||
if successor is None:
|
||||
continue
|
||||
new_data = self._prepare_update_data(successor, data, old_data, alias_mapping)
|
||||
if len(new_data) == 0:
|
||||
continue
|
||||
new_old_data = child_schema.update_cst(successor.pk, new_data)
|
||||
if len(new_old_data) == 0:
|
||||
continue
|
||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||
self._cascade_update_cst(
|
||||
operation=child_id,
|
||||
cst_id=successor_id,
|
||||
data=new_data,
|
||||
old_data=new_old_data,
|
||||
mapping=new_mapping
|
||||
)
|
||||
|
||||
def _cascade_delete_inherited(self, operation: int, target: list[int]) -> None:
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
self._execute_delete_inherited(child_id, target)
|
||||
|
||||
def _execute_delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None:
|
||||
operation = self.cache.operation_by_id[operation_id]
|
||||
schema = self.cache.get_schema(operation)
|
||||
if schema is None:
|
||||
return
|
||||
self._undo_substitutions_cst(parent_ids, operation, schema)
|
||||
target_ids = self.cache.get_inheritors_list(parent_ids, operation_id)
|
||||
self._cascade_delete_inherited(operation_id, target_ids)
|
||||
if len(target_ids) > 0:
|
||||
self.cache.remove_cst(operation_id, target_ids)
|
||||
schema.delete_cst(target_ids)
|
||||
|
||||
def _cascade_before_substitute(self, substitutions: CstSubstitution, operation: Operation) -> None:
|
||||
children = self.cache.graph.outputs[operation.pk]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
||||
if len(new_substitutions) == 0:
|
||||
continue
|
||||
self._cascade_before_substitute(new_substitutions, child_operation)
|
||||
child_schema.substitute(new_substitutions)
|
||||
|
||||
def _cascade_partial_mapping(
|
||||
self,
|
||||
mapping: CstMapping,
|
||||
target: list[int],
|
||||
operation: int,
|
||||
schema: RSFormCached
|
||||
) -> None:
|
||||
alias_mapping = OperationSchemaCached._produce_alias_mapping(mapping)
|
||||
schema.apply_partial_mapping(alias_mapping, target)
|
||||
children = self.cache.graph.outputs[operation]
|
||||
if len(children) == 0:
|
||||
return
|
||||
self.cache.ensure_loaded_subs()
|
||||
for child_id in children:
|
||||
child_operation = self.cache.operation_by_id[child_id]
|
||||
child_schema = self.cache.get_schema(child_operation)
|
||||
if child_schema is None:
|
||||
continue
|
||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||
if not new_mapping:
|
||||
continue
|
||||
new_target = self.cache.get_inheritors_list(target, child_id)
|
||||
if len(new_target) == 0:
|
||||
continue
|
||||
self._cascade_partial_mapping(new_mapping, new_target, child_id, child_schema)
|
||||
|
||||
@staticmethod
|
||||
def _produce_alias_mapping(mapping: CstMapping) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for alias, cst in mapping.items():
|
||||
if cst is None:
|
||||
result[alias] = DELETED_ALIAS
|
||||
else:
|
||||
result[alias] = cst.alias
|
||||
return result
|
||||
|
||||
def _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping:
|
||||
if len(mapping) == 0:
|
||||
return mapping
|
||||
result: CstMapping = {}
|
||||
for alias, cst in mapping.items():
|
||||
if cst is None:
|
||||
result[alias] = None
|
||||
continue
|
||||
successor_id = self.cache.get_successor(cst.pk, operation.pk)
|
||||
if successor_id is None:
|
||||
continue
|
||||
successor = schema.cache.by_id.get(successor_id)
|
||||
if successor is None:
|
||||
continue
|
||||
result[alias] = successor
|
||||
return result
|
||||
|
||||
def _determine_insert_position(
|
||||
self, prototype_id: int,
|
||||
operation: Operation,
|
||||
source: RSFormCached,
|
||||
destination: RSFormCached
|
||||
) -> int:
|
||||
''' Determine insert_after for new constituenta. '''
|
||||
prototype = source.cache.by_id[prototype_id]
|
||||
prototype_index = source.cache.constituents.index(prototype)
|
||||
if prototype_index == 0:
|
||||
return 0
|
||||
prev_cst = source.cache.constituents[prototype_index - 1]
|
||||
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
||||
if inherited_prev_id is None:
|
||||
return INSERT_LAST
|
||||
prev_cst = destination.cache.by_id[inherited_prev_id]
|
||||
prev_index = destination.cache.constituents.index(prev_cst)
|
||||
return prev_index + 1
|
||||
|
||||
def _extract_data_references(self, data: dict, old_data: dict) -> set[str]:
|
||||
result: set[str] = set()
|
||||
if 'definition_formal' in data:
|
||||
result.update(extract_globals(data['definition_formal']))
|
||||
result.update(extract_globals(old_data['definition_formal']))
|
||||
if 'term_raw' in data:
|
||||
result.update(extract_entities(data['term_raw']))
|
||||
result.update(extract_entities(old_data['term_raw']))
|
||||
if 'definition_raw' in data:
|
||||
result.update(extract_entities(data['definition_raw']))
|
||||
result.update(extract_entities(old_data['definition_raw']))
|
||||
return result
|
||||
|
||||
def _prepare_update_data(self, cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
|
||||
new_data = {}
|
||||
if 'term_forms' in data:
|
||||
if old_data['term_forms'] == cst.term_forms:
|
||||
new_data['term_forms'] = data['term_forms']
|
||||
if 'convention' in data:
|
||||
new_data['convention'] = data['convention']
|
||||
if 'definition_formal' in data:
|
||||
new_data['definition_formal'] = replace_globals(data['definition_formal'], mapping)
|
||||
if 'term_raw' in data:
|
||||
if replace_entities(old_data['term_raw'], mapping) == cst.term_raw:
|
||||
new_data['term_raw'] = replace_entities(data['term_raw'], mapping)
|
||||
if 'definition_raw' in data:
|
||||
if replace_entities(old_data['definition_raw'], mapping) == cst.definition_raw:
|
||||
new_data['definition_raw'] = replace_entities(data['definition_raw'], mapping)
|
||||
return new_data
|
||||
|
||||
def _transform_substitutions(
|
||||
self,
|
||||
target: CstSubstitution,
|
||||
operation: int,
|
||||
schema: RSFormCached
|
||||
) -> CstSubstitution:
|
||||
result: CstSubstitution = []
|
||||
for current_sub in target:
|
||||
sub_replaced = False
|
||||
new_substitution_id = self.cache.get_inheritor(current_sub[1].pk, operation)
|
||||
if new_substitution_id is None:
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.original_id == current_sub[1].pk:
|
||||
sub_replaced = True
|
||||
new_substitution_id = self.cache.get_inheritor(sub.original_id, operation)
|
||||
break
|
||||
|
||||
new_original_id = self.cache.get_inheritor(current_sub[0].pk, operation)
|
||||
original_replaced = False
|
||||
if new_original_id is None:
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.original_id == current_sub[0].pk:
|
||||
original_replaced = True
|
||||
sub.original_id = current_sub[1].pk
|
||||
sub.save()
|
||||
new_original_id = new_substitution_id
|
||||
new_substitution_id = self.cache.get_inheritor(sub.substitution_id, operation)
|
||||
break
|
||||
|
||||
if sub_replaced and original_replaced:
|
||||
raise ValidationError({'propagation': 'Substitution breaks OSS substitutions.'})
|
||||
|
||||
for sub in self.cache.substitutions[operation]:
|
||||
if sub.substitution_id == current_sub[0].pk:
|
||||
sub.substitution_id = current_sub[1].pk
|
||||
sub.save()
|
||||
|
||||
if new_original_id is not None and new_substitution_id is not None:
|
||||
result.append((schema.cache.by_id[new_original_id], schema.cache.by_id[new_substitution_id]))
|
||||
return result
|
||||
|
||||
def _undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
|
||||
to_process = []
|
||||
for sub in self.cache.substitutions[operation.pk]:
|
||||
if sub.original_id in target_ids or sub.substitution_id in target_ids:
|
||||
to_process.append(sub)
|
||||
for sub in to_process:
|
||||
self._undo_substitution(schema, sub, target_ids)
|
||||
|
||||
def _undo_substitution(
|
||||
self,
|
||||
schema: RSFormCached,
|
||||
target: Substitution,
|
||||
ignore_parents: Optional[list[int]] = None
|
||||
) -> None:
|
||||
if ignore_parents is None:
|
||||
ignore_parents = []
|
||||
operation_id = target.operation_id
|
||||
original_schema, _, original_cst, substitution_cst = self.cache.unfold_sub(target)
|
||||
|
||||
dependant = []
|
||||
for cst_id in original_schema.get_dependant([original_cst.pk]):
|
||||
if cst_id not in ignore_parents:
|
||||
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
||||
if inheritor_id is not None:
|
||||
dependant.append(inheritor_id)
|
||||
|
||||
self.cache.substitutions[operation_id].remove(target)
|
||||
target.delete()
|
||||
|
||||
new_original: Optional[Constituenta] = None
|
||||
if original_cst.pk not in ignore_parents:
|
||||
full_cst = Constituenta.objects.get(pk=original_cst.pk)
|
||||
self.after_create_cst(original_schema, [full_cst])
|
||||
new_original_id = self.cache.get_inheritor(original_cst.pk, operation_id)
|
||||
assert new_original_id is not None
|
||||
new_original = schema.cache.by_id[new_original_id]
|
||||
if len(dependant) == 0:
|
||||
return
|
||||
|
||||
substitution_id = self.cache.get_inheritor(substitution_cst.pk, operation_id)
|
||||
assert substitution_id is not None
|
||||
substitution_inheritor = schema.cache.by_id[substitution_id]
|
||||
mapping = {substitution_inheritor.alias: new_original}
|
||||
self._cascade_partial_mapping(mapping, dependant, operation_id, schema)
|
||||
|
||||
def _process_added_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
||||
if len(added) == 0:
|
||||
return
|
||||
if schema is None:
|
||||
for sub in added:
|
||||
self.cache.insert_substitution(sub)
|
||||
return
|
||||
|
||||
cst_mapping: CstSubstitution = []
|
||||
for sub in added:
|
||||
original_id = self.cache.get_inheritor(sub.original_id, sub.operation_id)
|
||||
substitution_id = self.cache.get_inheritor(sub.substitution_id, sub.operation_id)
|
||||
if original_id is None or substitution_id is None:
|
||||
raise ValueError('Substitutions not found.')
|
||||
original_cst = schema.cache.by_id[original_id]
|
||||
substitution_cst = schema.cache.by_id[substitution_id]
|
||||
cst_mapping.append((original_cst, substitution_cst))
|
||||
self.before_substitute(schema.model.pk, cst_mapping)
|
||||
schema.substitute(cst_mapping)
|
||||
for sub in added:
|
||||
self.cache.insert_substitution(sub)
|
||||
|
||||
|
||||
class OssCache:
|
||||
''' Cache for OSS data. '''
|
||||
|
||||
def __init__(self, oss: OperationSchemaCached):
|
||||
self._oss = oss
|
||||
self._schemas: list[RSFormCached] = []
|
||||
self._schema_by_id: dict[int, RSFormCached] = {}
|
||||
|
||||
self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id', 'operation_type'))
|
||||
self.operation_by_id = {operation.pk: operation for operation in self.operations}
|
||||
self.graph = Graph[int]()
|
||||
for operation in self.operations:
|
||||
self.graph.add_node(operation.pk)
|
||||
|
||||
references = Reference.objects.filter(reference__oss=self._oss.model).only('reference_id', 'target_id')
|
||||
self.reference_target = {ref.reference_id: ref.target_id for ref in references}
|
||||
arguments = Argument.objects \
|
||||
.filter(operation__oss=self._oss.model) \
|
||||
.only('operation_id', 'argument_id') \
|
||||
.order_by('order')
|
||||
for argument in arguments:
|
||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||
target = self.reference_target.get(argument.argument_id)
|
||||
if target is not None:
|
||||
self.graph.add_edge(target, argument.operation_id)
|
||||
|
||||
self.is_loaded_subs = False
|
||||
self.substitutions: dict[int, list[Substitution]] = {}
|
||||
self.inheritance: dict[int, list[Inheritance]] = {}
|
||||
|
||||
def ensure_loaded_subs(self) -> None:
|
||||
''' Ensure cache is fully loaded. '''
|
||||
if self.is_loaded_subs:
|
||||
return
|
||||
self.is_loaded_subs = True
|
||||
for operation in self.operations:
|
||||
self.inheritance[operation.pk] = []
|
||||
self.substitutions[operation.pk] = []
|
||||
for sub in Substitution.objects.filter(operation__oss=self._oss.model).only(
|
||||
'operation_id', 'original_id', 'substitution_id'):
|
||||
self.substitutions[sub.operation_id].append(sub)
|
||||
for item in Inheritance.objects.filter(operation__oss=self._oss.model).only(
|
||||
'operation_id', 'parent_id', 'child_id'):
|
||||
self.inheritance[item.operation_id].append(item)
|
||||
|
||||
def get_schema(self, operation: Operation) -> Optional[RSFormCached]:
|
||||
''' Get schema by Operation. '''
|
||||
if operation.result_id is None:
|
||||
return None
|
||||
if operation.result_id in self._schema_by_id:
|
||||
return self._schema_by_id[operation.result_id]
|
||||
else:
|
||||
schema = RSFormCached.from_id(operation.result_id)
|
||||
schema.cache.ensure_loaded()
|
||||
self._insert_new(schema)
|
||||
return schema
|
||||
|
||||
def get_operation(self, schemaID: int) -> Operation:
|
||||
''' Get operation by schema. '''
|
||||
for operation in self.operations:
|
||||
if operation.result_id == schemaID and operation.operation_type != OperationType.REFERENCE:
|
||||
return operation
|
||||
raise ValueError(f'Operation for schema {schemaID} not found')
|
||||
|
||||
def get_inheritor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||
''' Get child for parent inside target RSFrom. '''
|
||||
for item in self.inheritance[operation]:
|
||||
if item.parent_id == parent_cst:
|
||||
return item.child_id
|
||||
return None
|
||||
|
||||
def get_inheritors_list(self, target: list[int], operation: int) -> list[int]:
|
||||
''' Get child for parent inside target RSFrom. '''
|
||||
result = []
|
||||
for item in self.inheritance[operation]:
|
||||
if item.parent_id in target:
|
||||
result.append(item.child_id)
|
||||
return result
|
||||
|
||||
def get_successor(self, parent_cst: int, operation: int) -> Optional[int]:
|
||||
''' Get child for parent inside target RSFrom including substitutions. '''
|
||||
for sub in self.substitutions[operation]:
|
||||
if sub.original_id == parent_cst:
|
||||
return self.get_inheritor(sub.substitution_id, operation)
|
||||
return self.get_inheritor(parent_cst, operation)
|
||||
|
||||
def insert_schema(self, schema: RSFormCached) -> None:
|
||||
''' Insert new schema. '''
|
||||
if not self._schema_by_id.get(schema.model.pk):
|
||||
schema.cache.ensure_loaded()
|
||||
self._insert_new(schema)
|
||||
|
||||
def insert_argument(self, argument: Argument) -> None:
|
||||
''' Insert new argument. '''
|
||||
self.graph.add_edge(argument.argument_id, argument.operation_id)
|
||||
target = self.reference_target.get(argument.argument_id)
|
||||
if target is not None:
|
||||
self.graph.add_edge(target, argument.operation_id)
|
||||
|
||||
def insert_inheritance(self, inheritance: Inheritance) -> None:
|
||||
''' Insert new inheritance. '''
|
||||
self.inheritance[inheritance.operation_id].append(inheritance)
|
||||
|
||||
def insert_substitution(self, sub: Substitution) -> None:
|
||||
''' Insert new substitution. '''
|
||||
self.substitutions[sub.operation_id].append(sub)
|
||||
|
||||
def remove_cst(self, operation: int, target: list[int]) -> None:
|
||||
''' Remove constituents from operation. '''
|
||||
subs_to_delete = [
|
||||
sub for sub in self.substitutions[operation]
|
||||
if sub.original_id in target or sub.substitution_id in target
|
||||
]
|
||||
for sub in subs_to_delete:
|
||||
self.substitutions[operation].remove(sub)
|
||||
inherit_to_delete = [item for item in self.inheritance[operation] if item.child_id in target]
|
||||
for item in inherit_to_delete:
|
||||
self.inheritance[operation].remove(item)
|
||||
|
||||
def remove_schema(self, schema: RSFormCached) -> None:
|
||||
''' Remove schema from cache. '''
|
||||
self._schemas.remove(schema)
|
||||
del self._schema_by_id[schema.model.pk]
|
||||
|
||||
def remove_operation(self, operation: int) -> None:
|
||||
''' Remove operation from cache. '''
|
||||
target = self.operation_by_id[operation]
|
||||
self.graph.remove_node(operation)
|
||||
if target.result_id in self._schema_by_id:
|
||||
self._schemas.remove(self._schema_by_id[target.result_id])
|
||||
del self._schema_by_id[target.result_id]
|
||||
self.operations.remove(self.operation_by_id[operation])
|
||||
del self.operation_by_id[operation]
|
||||
if operation in self.reference_target:
|
||||
del self.reference_target[operation]
|
||||
if self.is_loaded_subs:
|
||||
del self.substitutions[operation]
|
||||
del self.inheritance[operation]
|
||||
|
||||
def remove_argument(self, argument: Argument) -> None:
|
||||
''' Remove argument from cache. '''
|
||||
self.graph.remove_edge(argument.argument_id, argument.operation_id)
|
||||
target = self.reference_target.get(argument.argument_id)
|
||||
if target is not None:
|
||||
if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
|
||||
self.graph.remove_edge(target, argument.operation_id)
|
||||
|
||||
def remove_substitution(self, target: Substitution) -> None:
|
||||
''' Remove substitution from cache. '''
|
||||
self.substitutions[target.operation_id].remove(target)
|
||||
|
||||
def remove_inheritance(self, target: Inheritance) -> None:
|
||||
''' Remove inheritance from cache. '''
|
||||
self.inheritance[target.operation_id].remove(target)
|
||||
|
||||
def unfold_sub(self, sub: Substitution) -> tuple[RSFormCached, RSFormCached, Constituenta, Constituenta]:
|
||||
''' Unfold substitution into original and substitution forms. '''
|
||||
operation = self.operation_by_id[sub.operation_id]
|
||||
parents = self.graph.inputs[operation.pk]
|
||||
original_cst = None
|
||||
substitution_cst = None
|
||||
original_schema = None
|
||||
substitution_schema = None
|
||||
for parent_id in parents:
|
||||
parent_schema = self.get_schema(self.operation_by_id[parent_id])
|
||||
if parent_schema is None:
|
||||
continue
|
||||
if sub.original_id in parent_schema.cache.by_id:
|
||||
original_schema = parent_schema
|
||||
original_cst = original_schema.cache.by_id[sub.original_id]
|
||||
if sub.substitution_id in parent_schema.cache.by_id:
|
||||
substitution_schema = parent_schema
|
||||
substitution_cst = substitution_schema.cache.by_id[sub.substitution_id]
|
||||
if original_schema is None or substitution_schema is None or original_cst is None or substitution_cst is None:
|
||||
raise ValueError(f'Parent schema for Substitution-{sub.pk} not found.')
|
||||
return original_schema, substitution_schema, original_cst, substitution_cst
|
||||
|
||||
def _insert_new(self, schema: RSFormCached) -> None:
|
||||
self._schemas.append(schema)
|
||||
self._schema_by_id[schema.model.pk] = schema
|
|
@ -2,79 +2,73 @@
|
|||
from typing import Optional
|
||||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from apps.rsform.models import Constituenta, CstType, RSFormCached
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
|
||||
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
||||
from .OperationSchema import CstSubstitution, OperationSchema
|
||||
|
||||
|
||||
def _get_oss_hosts(schemaID: int) -> list[LibraryItem]:
|
||||
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]:
|
||||
''' Get all hosts for LibraryItem. '''
|
||||
return list(LibraryItem.objects.filter(operations__result_id=schemaID).only('pk').distinct())
|
||||
return list(LibraryItem.objects.filter(operations__result=item).only('pk'))
|
||||
|
||||
|
||||
class PropagationFacade:
|
||||
''' Change propagation API. '''
|
||||
|
||||
@staticmethod
|
||||
def after_create_cst(source: RSFormCached, new_cst: list[Constituenta],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions when new constituenta is created. '''
|
||||
hosts = _get_oss_hosts(source.model.pk)
|
||||
hosts = _get_oss_hosts(source.model)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).after_create_cst(source, new_cst)
|
||||
OperationSchema(host).after_create_cst(source, new_cst)
|
||||
|
||||
@staticmethod
|
||||
def after_change_cst_type(sourceID: int, target: int, new_type: CstType,
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
def after_change_cst_type(source: RSForm, target: Constituenta, exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||
hosts = _get_oss_hosts(sourceID)
|
||||
hosts = _get_oss_hosts(source.model)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).after_change_cst_type(sourceID, target, new_type)
|
||||
OperationSchema(host).after_change_cst_type(source, target)
|
||||
|
||||
@staticmethod
|
||||
def after_update_cst(
|
||||
source: RSFormCached,
|
||||
target: int,
|
||||
source: RSForm,
|
||||
target: Constituenta,
|
||||
data: dict,
|
||||
old_data: dict,
|
||||
exclude: Optional[list[int]] = None
|
||||
) -> None:
|
||||
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||
hosts = _get_oss_hosts(source.model.pk)
|
||||
hosts = _get_oss_hosts(source.model)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).after_update_cst(source, target, data, old_data)
|
||||
OperationSchema(host).after_update_cst(source, target, data, old_data)
|
||||
|
||||
@staticmethod
|
||||
def before_delete_cst(sourceID: int, target: list[int],
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
def before_delete_cst(source: RSForm, target: list[Constituenta], exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions before constituents are deleted. '''
|
||||
hosts = _get_oss_hosts(sourceID)
|
||||
hosts = _get_oss_hosts(source.model)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).before_delete_cst(sourceID, target)
|
||||
OperationSchema(host).before_delete_cst(source, target)
|
||||
|
||||
@staticmethod
|
||||
def before_substitute(sourceID: int, substitutions: CstSubstitution,
|
||||
exclude: Optional[list[int]] = None) -> None:
|
||||
def before_substitute(source: RSForm, substitutions: CstSubstitution, exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions before constituents are substituted. '''
|
||||
if len(substitutions) == 0:
|
||||
return
|
||||
hosts = _get_oss_hosts(sourceID)
|
||||
hosts = _get_oss_hosts(source.model)
|
||||
for host in hosts:
|
||||
if exclude is None or host.pk not in exclude:
|
||||
OperationSchemaCached(host).before_substitute(sourceID, substitutions)
|
||||
OperationSchema(host).before_substitute(source, substitutions)
|
||||
|
||||
@staticmethod
|
||||
def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None:
|
||||
''' Trigger cascade resolutions before schema is deleted. '''
|
||||
if item.item_type != LibraryItemType.RSFORM:
|
||||
return
|
||||
hosts = _get_oss_hosts(item.pk)
|
||||
hosts = _get_oss_hosts(item)
|
||||
if len(hosts) == 0:
|
||||
return
|
||||
|
||||
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
|
||||
PropagationFacade.before_delete_cst(item.pk, ids, exclude)
|
||||
schema = RSForm(item)
|
||||
PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order')), exclude)
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
''' Models: Operation Reference in OSS. '''
|
||||
from django.db.models import CASCADE, ForeignKey, Model
|
||||
|
||||
|
||||
class Reference(Model):
|
||||
''' Operation Reference. '''
|
||||
reference = ForeignKey(
|
||||
verbose_name='Отсылка',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='references'
|
||||
)
|
||||
target = ForeignKey(
|
||||
verbose_name='Целевая Операция',
|
||||
to='oss.Operation',
|
||||
on_delete=CASCADE,
|
||||
related_name='targets'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
verbose_name = 'Отсылка'
|
||||
verbose_name_plural = 'Отсылки'
|
||||
unique_together = [['reference', 'target']]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.reference} -> {self.target}'
|
|
@ -6,7 +6,5 @@ from .Inheritance import Inheritance
|
|||
from .Layout import Layout
|
||||
from .Operation import Operation, OperationType
|
||||
from .OperationSchema import OperationSchema
|
||||
from .OperationSchemaCached import OperationSchemaCached
|
||||
from .PropagationFacade import PropagationFacade
|
||||
from .Reference import Reference
|
||||
from .Substitution import Substitution
|
||||
|
|
|
@ -4,19 +4,15 @@ from .basics import LayoutSerializer, SubstitutionExSerializer
|
|||
from .data_access import (
|
||||
ArgumentSerializer,
|
||||
BlockSerializer,
|
||||
CloneSchemaSerializer,
|
||||
CreateBlockSerializer,
|
||||
CreateReferenceSerializer,
|
||||
CreateSchemaSerializer,
|
||||
CreateSynthesisSerializer,
|
||||
DeleteBlockSerializer,
|
||||
DeleteOperationSerializer,
|
||||
DeleteReferenceSerializer,
|
||||
ImportSchemaSerializer,
|
||||
MoveItemsSerializer,
|
||||
OperationSchemaSerializer,
|
||||
OperationSerializer,
|
||||
ReferenceSerializer,
|
||||
RelocateConstituentsSerializer,
|
||||
SetOperationInputSerializer,
|
||||
TargetOperationSerializer,
|
||||
|
|
|
@ -13,16 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase
|
|||
from shared import messages as msg
|
||||
from shared.serializers import StrictModelSerializer, StrictSerializer
|
||||
|
||||
from ..models import (
|
||||
Argument,
|
||||
Block,
|
||||
Inheritance,
|
||||
Layout,
|
||||
Operation,
|
||||
OperationType,
|
||||
Reference,
|
||||
Substitution
|
||||
)
|
||||
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
|
||||
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
|
||||
|
||||
|
||||
|
@ -54,14 +45,6 @@ class ArgumentSerializer(StrictModelSerializer):
|
|||
fields = ('operation', 'argument')
|
||||
|
||||
|
||||
class ReferenceSerializer(StrictModelSerializer):
|
||||
''' Serializer: Reference data. '''
|
||||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = Reference
|
||||
fields = ('reference', 'target')
|
||||
|
||||
|
||||
class CreateBlockSerializer(StrictSerializer):
|
||||
''' Serializer: Block creation. '''
|
||||
class BlockCreateData(StrictModelSerializer):
|
||||
|
@ -234,50 +217,6 @@ class CreateSchemaSerializer(StrictSerializer):
|
|||
return attrs
|
||||
|
||||
|
||||
class CloneSchemaSerializer(StrictSerializer):
|
||||
''' Serializer: Clone schema. '''
|
||||
layout = serializers.ListField(child=NodeSerializer())
|
||||
source_operation = PKField(many=False, queryset=Operation.objects.all())
|
||||
position = PositionSerializer()
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
source_operation = cast(Operation, attrs['source_operation'])
|
||||
if source_operation.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'source_operation': msg.operationNotInOSS()
|
||||
})
|
||||
if source_operation.result is None:
|
||||
raise serializers.ValidationError({
|
||||
'source_operation': msg.operationResultEmpty(source_operation.alias)
|
||||
})
|
||||
if source_operation.operation_type == OperationType.REFERENCE:
|
||||
raise serializers.ValidationError({
|
||||
'source_operation': msg.referenceTypeNotAllowed()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class CreateReferenceSerializer(StrictSerializer):
|
||||
''' Serializer: Create reference operation. '''
|
||||
layout = serializers.ListField(child=NodeSerializer())
|
||||
target = PKField(many=False, queryset=Operation.objects.all())
|
||||
position = PositionSerializer()
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
target = cast(Operation, attrs['target'])
|
||||
if target.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'target_operation': msg.operationNotInOSS()
|
||||
})
|
||||
if target.operation_type == OperationType.REFERENCE:
|
||||
raise serializers.ValidationError({
|
||||
'target_operation': msg.referenceTypeNotAllowed()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class ImportSchemaSerializer(StrictSerializer):
|
||||
''' Serializer: Import schema to new operation. '''
|
||||
layout = serializers.ListField(child=NodeSerializer())
|
||||
|
@ -308,7 +247,7 @@ class CreateSynthesisSerializer(StrictSerializer):
|
|||
|
||||
arguments = PKField(
|
||||
many=True,
|
||||
queryset=Operation.objects.all().only('pk', 'result_id')
|
||||
queryset=Operation.objects.all().only('pk')
|
||||
)
|
||||
substitutions = serializers.ListField(
|
||||
child=SubstitutionSerializerBase(),
|
||||
|
@ -432,11 +371,11 @@ class UpdateOperationSerializer(StrictSerializer):
|
|||
|
||||
|
||||
class DeleteOperationSerializer(StrictSerializer):
|
||||
''' Serializer: Delete non-reference operation. '''
|
||||
''' Serializer: Delete operation. '''
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type', 'result'))
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result'))
|
||||
keep_constituents = serializers.BooleanField(default=False, required=False)
|
||||
delete_schema = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
|
@ -447,33 +386,6 @@ class DeleteOperationSerializer(StrictSerializer):
|
|||
raise serializers.ValidationError({
|
||||
'target': msg.operationNotInOSS()
|
||||
})
|
||||
if operation.operation_type == OperationType.REFERENCE:
|
||||
raise serializers.ValidationError({
|
||||
'target': msg.referenceTypeNotAllowed()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class DeleteReferenceSerializer(StrictSerializer):
|
||||
''' Serializer: Delete reference operation. '''
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type'))
|
||||
keep_connections = serializers.BooleanField(default=False, required=False)
|
||||
keep_constituents = serializers.BooleanField(default=False, required=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
oss = cast(LibraryItem, self.context['oss'])
|
||||
operation = cast(Operation, attrs['target'])
|
||||
if operation.oss_id != oss.pk:
|
||||
raise serializers.ValidationError({
|
||||
'target': msg.operationNotInOSS()
|
||||
})
|
||||
if operation.operation_type != OperationType.REFERENCE:
|
||||
raise serializers.ValidationError({
|
||||
'target': msg.referenceTypeRequired()
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
|
@ -535,9 +447,6 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
|||
substitutions = serializers.ListField(
|
||||
child=SubstitutionExSerializer()
|
||||
)
|
||||
references = serializers.ListField(
|
||||
child=ReferenceSerializer()
|
||||
)
|
||||
layout = serializers.ListField(
|
||||
child=NodeSerializer()
|
||||
)
|
||||
|
@ -550,13 +459,13 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
|||
def to_representation(self, instance: LibraryItem):
|
||||
result = LibraryItemDetailsSerializer(instance).data
|
||||
del result['versions']
|
||||
result['layout'] = Layout.objects.get(oss=instance).data
|
||||
oss = OperationSchema(instance)
|
||||
result['layout'] = oss.layout().data
|
||||
result['operations'] = []
|
||||
result['blocks'] = []
|
||||
result['arguments'] = []
|
||||
result['substitutions'] = []
|
||||
result['references'] = []
|
||||
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
|
||||
for operation in oss.operations().order_by('pk'):
|
||||
operation_data = OperationSerializer(operation).data
|
||||
operation_result = operation.result
|
||||
operation_data['is_import'] = \
|
||||
|
@ -564,11 +473,11 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
|||
(operation_result.owner_id != instance.owner_id or
|
||||
operation_result.location != instance.location)
|
||||
result['operations'].append(operation_data)
|
||||
for block in Block.objects.filter(oss=instance).order_by('pk'):
|
||||
for block in oss.blocks().order_by('pk'):
|
||||
result['blocks'].append(BlockSerializer(block).data)
|
||||
for argument in Argument.objects.filter(operation__oss=instance).order_by('order'):
|
||||
for argument in oss.arguments().order_by('order'):
|
||||
result['arguments'].append(ArgumentSerializer(argument).data)
|
||||
for substitution in Substitution.objects.filter(operation__oss=instance).values(
|
||||
for substitution in oss.substitutions().values(
|
||||
'operation',
|
||||
'original',
|
||||
'substitution',
|
||||
|
@ -578,9 +487,6 @@ class OperationSchemaSerializer(StrictModelSerializer):
|
|||
substitution_term=F('substitution__term_resolved'),
|
||||
).order_by('pk'):
|
||||
result['substitutions'].append(substitution)
|
||||
for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
|
||||
result['references'].append(ReferenceSerializer(reference).data)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
''' Tests for Django Models. '''
|
||||
from .t_Argument import *
|
||||
from .t_Inheritance import *
|
||||
from .t_Layout import *
|
||||
from .t_Operation import *
|
||||
from .t_Reference import *
|
||||
from .t_Substitution import *
|
||||
|
|
|
@ -20,8 +20,8 @@ class TestInheritance(TestCase):
|
|||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1.model
|
||||
)
|
||||
self.ks1_x1 = self.ks1.insert_last('X1')
|
||||
self.ks2_x1 = self.ks2.insert_last('X1')
|
||||
self.ks1_x1 = self.ks1.insert_new('X1')
|
||||
self.ks2_x1 = self.ks2.insert_new('X1')
|
||||
self.inheritance = Inheritance.objects.create(
|
||||
operation=self.operation,
|
||||
parent=self.ks1_x1,
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
''' Testing models: Layout. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.library.models import LibraryItem
|
||||
from apps.oss.models import Layout
|
||||
|
||||
|
||||
class TestLayout(TestCase):
|
||||
''' Testing Layout model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.library_item = LibraryItem.objects.create(alias='LIB1')
|
||||
self.layout = Layout.objects.create(
|
||||
oss=self.library_item,
|
||||
data=[{'x': 1, 'y': 2}]
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
expected = f'Схема расположения {self.library_item.alias}'
|
||||
self.assertEqual(str(self.layout), expected)
|
||||
|
||||
|
||||
def test_update_data(self):
|
||||
new_data = [{'x': 10, 'y': 20}]
|
||||
Layout.update_data(self.library_item.id, new_data)
|
||||
self.layout.refresh_from_db()
|
||||
self.assertEqual(self.layout.data, new_data)
|
||||
|
||||
|
||||
def test_default_data(self):
|
||||
layout2 = Layout.objects.create(oss=self.library_item)
|
||||
self.assertEqual(layout2.data, [])
|
||||
|
||||
|
||||
def test_related_name_layout(self):
|
||||
layouts = self.library_item.layout.all()
|
||||
self.assertIn(self.layout, layouts)
|
|
@ -3,6 +3,7 @@ from django.test import TestCase
|
|||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import RSForm
|
||||
|
||||
|
||||
class TestOperation(TestCase):
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
''' Testing models: Reference. '''
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType, Reference
|
||||
from apps.rsform.models import RSForm
|
||||
|
||||
|
||||
class TestReference(TestCase):
|
||||
''' Testing Reference model. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
self.operation1 = Operation.objects.create(
|
||||
oss=self.oss.model,
|
||||
alias='KS1',
|
||||
operation_type=OperationType.INPUT,
|
||||
)
|
||||
self.operation2 = Operation.objects.create(
|
||||
oss=self.oss.model,
|
||||
operation_type=OperationType.REFERENCE,
|
||||
)
|
||||
self.reference = Reference.objects.create(
|
||||
reference=self.operation2,
|
||||
target=self.operation1
|
||||
)
|
||||
|
||||
|
||||
def test_str(self):
|
||||
testStr = f'{self.operation2} -> {self.operation1}'
|
||||
self.assertEqual(str(self.reference), testStr)
|
||||
|
||||
|
||||
def test_cascade_delete_operation(self):
|
||||
self.assertEqual(Reference.objects.count(), 1)
|
||||
self.operation2.delete()
|
||||
self.assertEqual(Reference.objects.count(), 0)
|
||||
|
||||
|
||||
def test_cascade_delete_target(self):
|
||||
self.assertEqual(Reference.objects.count(), 1)
|
||||
self.operation1.delete()
|
||||
self.assertEqual(Reference.objects.count(), 0)
|
|
@ -15,9 +15,9 @@ class TestSynthesisSubstitution(TestCase):
|
|||
self.oss = OperationSchema.create(alias='T1')
|
||||
|
||||
self.ks1 = RSForm.create(alias='KS1', title='Test1')
|
||||
self.ks1X1 = self.ks1.insert_last('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.ks2X1 = self.ks2.insert_last('X2', term_resolved='X1_2')
|
||||
self.ks2X1 = self.ks2.insert_new('X2', term_resolved='X1_2')
|
||||
|
||||
self.operation1 = Operation.objects.create(
|
||||
oss=self.oss.model,
|
||||
|
|
|
@ -2,5 +2,4 @@
|
|||
from .t_attributes import *
|
||||
from .t_constituents import *
|
||||
from .t_operations import *
|
||||
from .t_references import *
|
||||
from .t_substitutions import *
|
||||
|
|
|
@ -64,7 +64,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
@ -75,10 +75,10 @@ class TestChangeAttributes(EndpointTester):
|
|||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks2.model.refresh_from_db()
|
||||
self.ks3.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.owner, self.user3)
|
||||
self.assertEqual(self.ks1.model.owner, self.user)
|
||||
self.assertEqual(self.ks2.model.owner, self.user2)
|
||||
|
@ -91,10 +91,10 @@ class TestChangeAttributes(EndpointTester):
|
|||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks2.model.refresh_from_db()
|
||||
self.ks3.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.location, data['location'])
|
||||
self.assertNotEqual(self.ks1.model.location, data['location'])
|
||||
self.assertNotEqual(self.ks2.model.location, data['location'])
|
||||
|
@ -107,10 +107,10 @@ class TestChangeAttributes(EndpointTester):
|
|||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks2.model.refresh_from_db()
|
||||
self.ks3.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.owned.model.access_policy, data['access_policy'])
|
||||
self.assertNotEqual(self.ks1.model.access_policy, data['access_policy'])
|
||||
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
|
||||
|
@ -126,10 +126,10 @@ class TestChangeAttributes(EndpointTester):
|
|||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
|
||||
self.owned.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks2.model.refresh_from_db()
|
||||
self.ks3.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks2.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(list(self.owned.model.getQ_editors()), [self.user3])
|
||||
self.assertEqual(list(self.ks1.model.getQ_editors()), [self.user, self.user2])
|
||||
self.assertEqual(list(self.ks2.model.getQ_editors()), [])
|
||||
|
@ -162,7 +162,7 @@ class TestChangeAttributes(EndpointTester):
|
|||
}
|
||||
|
||||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks3.model.refresh_from_db()
|
||||
self.ks3.refresh_from_db()
|
||||
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
|
||||
self.assertEqual(self.ks3.model.title, data['item_data']['title'])
|
||||
self.assertEqual(self.ks3.model.description, data['item_data']['description'])
|
||||
|
|
|
@ -22,16 +22,16 @@ class TestChangeConstituents(EndpointTester):
|
|||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last('X4')
|
||||
self.ks1X2 = self.ks1.insert_last('X5')
|
||||
self.ks1X1 = self.ks1.insert_new('X4')
|
||||
self.ks1X2 = self.ks1.insert_new('X5')
|
||||
|
||||
self.ks2 = RSForm.create(
|
||||
alias='KS2',
|
||||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last('X1')
|
||||
self.ks2D1 = self.ks2.insert_last(
|
||||
self.ks2X1 = self.ks2.insert_new('X1')
|
||||
self.ks2D1 = self.ks2.insert_new(
|
||||
alias='D1',
|
||||
definition_formal=r'X1\X1'
|
||||
)
|
||||
|
@ -55,14 +55,14 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.owned.execute_operation(self.operation3)
|
||||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
@ -105,8 +105,8 @@ class TestChangeConstituents(EndpointTester):
|
|||
response = self.executeCreated(data=data, schema=self.ks1.model.pk)
|
||||
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 5)
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
self.assertEqual(self.ks3.constituents().count(), 5)
|
||||
self.assertEqual(inherited_cst.alias, 'X4')
|
||||
self.assertEqual(inherited_cst.order, 2)
|
||||
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
|
||||
|
@ -114,15 +114,14 @@ class TestChangeConstituents(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
||||
def test_update_constituenta(self):
|
||||
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
|
||||
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
|
||||
data = {
|
||||
'target': self.ks1X1.pk,
|
||||
'item_data': {
|
||||
'term_raw': 'Test1',
|
||||
'definition_formal': r'X4\X4',
|
||||
'definition_raw': '@{X5|sing,datv}',
|
||||
'convention': 'test',
|
||||
'crucial': True,
|
||||
'convention': 'test'
|
||||
}
|
||||
}
|
||||
response = self.executeOK(data=data, schema=self.ks1.model.pk)
|
||||
|
@ -133,11 +132,9 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.assertEqual(self.ks1X1.definition_formal, data['item_data']['definition_formal'])
|
||||
self.assertEqual(self.ks1X1.definition_raw, data['item_data']['definition_raw'])
|
||||
self.assertEqual(self.ks1X1.convention, data['item_data']['convention'])
|
||||
self.assertEqual(self.ks1X1.crucial, data['item_data']['crucial'])
|
||||
self.assertEqual(d2.definition_resolved, data['item_data']['term_raw'])
|
||||
self.assertEqual(inherited_cst.term_raw, data['item_data']['term_raw'])
|
||||
self.assertEqual(inherited_cst.convention, data['item_data']['convention'])
|
||||
self.assertEqual(inherited_cst.crucial, False)
|
||||
self.assertEqual(inherited_cst.definition_formal, r'X1\X1')
|
||||
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
||||
|
||||
|
@ -148,15 +145,15 @@ class TestChangeConstituents(EndpointTester):
|
|||
response = self.executeOK(data=data, schema=self.ks2.model.pk)
|
||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
||||
self.ks2D1.refresh_from_db()
|
||||
self.assertEqual(self.ks2.constituentsQ().count(), 1)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks2.constituents().count(), 1)
|
||||
self.assertEqual(self.ks3.constituents().count(), 3)
|
||||
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
|
||||
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
|
||||
def test_substitute(self):
|
||||
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
|
||||
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
|
||||
data = {'substitutions': [{
|
||||
'original': self.ks1X1.pk,
|
||||
'substitution': self.ks1X2.pk
|
||||
|
@ -164,7 +161,7 @@ class TestChangeConstituents(EndpointTester):
|
|||
self.executeOK(data=data, schema=self.ks1.model.pk)
|
||||
self.ks1X2.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 1)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks1.constituents().count(), 1)
|
||||
self.assertEqual(self.ks3.constituents().count(), 4)
|
||||
self.assertEqual(self.ks1X2.order, 0)
|
||||
self.assertEqual(d2.definition_formal, r'X2\X2\X3')
|
||||
|
|
|
@ -8,6 +8,7 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
|
|||
class TestChangeOperations(EndpointTester):
|
||||
''' Testing Operations change propagation in OSS. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.owned = OperationSchema.create(
|
||||
|
@ -22,18 +23,18 @@ class TestChangeOperations(EndpointTester):
|
|||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
|
||||
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
|
||||
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||
self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1')
|
||||
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2')
|
||||
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||
|
||||
self.ks2 = RSForm.create(
|
||||
alias='KS2',
|
||||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
|
||||
self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
|
||||
self.ks2S1 = self.ks2.insert_last(
|
||||
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1')
|
||||
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2')
|
||||
self.ks2S1 = self.ks2.insert_new(
|
||||
alias='S1',
|
||||
definition_formal=r'X1',
|
||||
convention='KS2S1'
|
||||
|
@ -44,8 +45,8 @@ class TestChangeOperations(EndpointTester):
|
|||
title='Test3',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks3X1 = self.ks3.insert_last('X1', convention='KS3X1')
|
||||
self.ks3D1 = self.ks3.insert_last(
|
||||
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1')
|
||||
self.ks3D1 = self.ks3.insert_new(
|
||||
alias='D1',
|
||||
definition_formal='X1 X1',
|
||||
convention='KS3D1'
|
||||
|
@ -82,7 +83,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
|
||||
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
|
||||
self.ks4D2 = self.ks4.insert_last(
|
||||
self.ks4D2 = self.ks4.insert_new(
|
||||
alias='D2',
|
||||
definition_formal=r'X1 X2 X3 S1 D1',
|
||||
convention='KS4D2'
|
||||
|
@ -100,7 +101,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.owned.execute_operation(self.operation5)
|
||||
self.operation5.refresh_from_db()
|
||||
self.ks5 = RSForm(self.operation5.result)
|
||||
self.ks5D4 = self.ks5.insert_last(
|
||||
self.ks5D4 = self.ks5.insert_new(
|
||||
alias='D4',
|
||||
definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
|
||||
convention='KS5D4'
|
||||
|
@ -113,17 +114,17 @@ class TestChangeOperations(EndpointTester):
|
|||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
def test_oss_setup(self):
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks2.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 2)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
self.assertEqual(self.ks2.constituents().count(), 3)
|
||||
self.assertEqual(self.ks3.constituents().count(), 2)
|
||||
self.assertEqual(self.ks4.constituents().count(), 6)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
|
||||
|
||||
|
||||
|
@ -141,8 +142,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
@ -165,8 +166,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
@ -179,9 +180,9 @@ class TestChangeOperations(EndpointTester):
|
|||
title='Test6',
|
||||
owner=self.user
|
||||
)
|
||||
ks6X1 = ks6.insert_last('X1', convention='KS6X1')
|
||||
ks6X2 = ks6.insert_last('X2', convention='KS6X2')
|
||||
ks6D1 = ks6.insert_last('D1', definition_formal='X1 X2', convention='KS6D1')
|
||||
ks6X1 = ks6.insert_new('X1', convention='KS6X1')
|
||||
ks6X2 = ks6.insert_new('X2', convention='KS6X2')
|
||||
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1')
|
||||
|
||||
data = {
|
||||
'layout': self.layout_data,
|
||||
|
@ -200,8 +201,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||
self.assertEqual(self.ks4.constituents().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 9)
|
||||
self.assertEqual(ks4Dks6.definition_formal, r'X5 X6')
|
||||
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
|
@ -219,8 +220,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 0)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
@ -241,8 +242,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 0)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
@ -263,8 +264,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||
self.assertEqual(self.ks4.constituents().count(), 6)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
|
||||
|
||||
|
@ -279,16 +280,16 @@ class TestChangeOperations(EndpointTester):
|
|||
}
|
||||
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
subs1_2 = self.operation4.getQ_substitutions()
|
||||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
self.assertEqual(self.ks4.constituents().count(), 6)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
|
||||
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1D1.pk).exists())
|
||||
|
@ -324,8 +325,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 2)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 5)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks4.constituents().count(), 5)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
|
||||
|
||||
|
@ -350,8 +351,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 4)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks4.constituents().count(), 4)
|
||||
self.assertEqual(self.ks5.constituents().count(), 6)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
@ -363,8 +364,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||
self.assertEqual(self.ks4.constituents().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 9)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
|
||||
|
||||
|
@ -373,9 +374,9 @@ class TestChangeOperations(EndpointTester):
|
|||
def test_execute_middle_operation(self):
|
||||
self.client.delete(f'/api/library/{self.ks4.model.pk}')
|
||||
self.operation4.refresh_from_db()
|
||||
self.ks5.model.refresh_from_db()
|
||||
self.ks5.refresh_from_db()
|
||||
self.assertEqual(self.operation4.result, None)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks5.constituents().count(), 3)
|
||||
|
||||
data = {
|
||||
'target': self.operation4.pk,
|
||||
|
@ -383,15 +384,15 @@ class TestChangeOperations(EndpointTester):
|
|||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.operation4.refresh_from_db()
|
||||
self.ks5.model.refresh_from_db()
|
||||
self.ks5.refresh_from_db()
|
||||
self.assertNotEqual(self.operation4.result, None)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||
def test_relocate_constituents_up(self):
|
||||
ks1_old_count = self.ks1.constituentsQ().count()
|
||||
ks4_old_count = self.ks4.constituentsQ().count()
|
||||
ks1_old_count = self.ks1.constituents().count()
|
||||
ks4_old_count = self.ks4.constituents().count()
|
||||
operation6 = self.owned.create_operation(
|
||||
alias='6',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
|
@ -400,8 +401,8 @@ class TestChangeOperations(EndpointTester):
|
|||
self.owned.execute_operation(operation6)
|
||||
operation6.refresh_from_db()
|
||||
ks6 = RSForm(operation6.result)
|
||||
ks6A1 = ks6.insert_last('A1')
|
||||
ks6_old_count = ks6.constituentsQ().count()
|
||||
ks6A1 = ks6.insert_new('A1')
|
||||
ks6_old_count = ks6.constituents().count()
|
||||
|
||||
data = {
|
||||
'destination': self.ks1.model.pk,
|
||||
|
@ -409,19 +410,19 @@ class TestChangeOperations(EndpointTester):
|
|||
}
|
||||
|
||||
self.executeOK(data=data)
|
||||
ks6.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks4.model.refresh_from_db()
|
||||
ks6.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks4.refresh_from_db()
|
||||
|
||||
self.assertEqual(ks6.constituentsQ().count(), ks6_old_count)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), ks1_old_count + 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1)
|
||||
self.assertEqual(ks6.constituents().count(), ks6_old_count)
|
||||
self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||
def test_relocate_constituents_down(self):
|
||||
ks1_old_count = self.ks1.constituentsQ().count()
|
||||
ks4_old_count = self.ks4.constituentsQ().count()
|
||||
ks1_old_count = self.ks1.constituents().count()
|
||||
ks4_old_count = self.ks4.constituents().count()
|
||||
|
||||
operation6 = self.owned.create_operation(
|
||||
alias='6',
|
||||
|
@ -431,7 +432,7 @@ class TestChangeOperations(EndpointTester):
|
|||
self.owned.execute_operation(operation6)
|
||||
operation6.refresh_from_db()
|
||||
ks6 = RSForm(operation6.result)
|
||||
ks6_old_count = ks6.constituentsQ().count()
|
||||
ks6_old_count = ks6.constituents().count()
|
||||
|
||||
data = {
|
||||
'destination': ks6.model.pk,
|
||||
|
@ -439,14 +440,14 @@ class TestChangeOperations(EndpointTester):
|
|||
}
|
||||
|
||||
self.executeOK(data=data)
|
||||
ks6.model.refresh_from_db()
|
||||
self.ks1.model.refresh_from_db()
|
||||
self.ks4.model.refresh_from_db()
|
||||
ks6.refresh_from_db()
|
||||
self.ks1.refresh_from_db()
|
||||
self.ks4.refresh_from_db()
|
||||
self.ks4D2.refresh_from_db()
|
||||
self.ks5D4.refresh_from_db()
|
||||
|
||||
self.assertEqual(ks6.constituentsQ().count(), ks6_old_count)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), ks1_old_count - 1)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count - 1)
|
||||
self.assertEqual(ks6.constituents().count(), ks6_old_count)
|
||||
self.assertEqual(self.ks1.constituents().count(), ks1_old_count - 1)
|
||||
self.assertEqual(self.ks4.constituents().count(), ks4_old_count - 1)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
''' Testing API: Propagate changes through references in OSS. '''
|
||||
|
||||
from apps.oss.models import OperationSchema, OperationType
|
||||
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
||||
|
||||
class ReferencePropagationTestCase(EndpointTester):
|
||||
''' Test propagation through references in OSS. '''
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.owned = OperationSchema.create(
|
||||
title='Test',
|
||||
alias='T1',
|
||||
owner=self.user
|
||||
)
|
||||
self.owned_id = self.owned.model.pk
|
||||
|
||||
self.ks1 = RSForm.create(
|
||||
alias='KS1',
|
||||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
|
||||
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
|
||||
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||
|
||||
self.ks2 = RSForm.create(
|
||||
alias='KS2',
|
||||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
|
||||
self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
|
||||
self.ks2S1 = self.ks2.insert_last(
|
||||
alias='S1',
|
||||
definition_formal=r'ℬ(X1)',
|
||||
convention='KS2S1'
|
||||
)
|
||||
|
||||
self.operation1 = self.owned.create_operation(
|
||||
alias='1',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks1.model
|
||||
)
|
||||
self.operation2 = self.owned.create_operation(
|
||||
alias='2',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=self.ks2.model
|
||||
)
|
||||
self.operation3 = self.owned.create_reference(self.operation1)
|
||||
|
||||
self.operation4 = self.owned.create_operation(
|
||||
alias='4',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2])
|
||||
self.owned.set_substitutions(self.operation4.pk, [{
|
||||
'original': self.ks1X1,
|
||||
'substitution': self.ks2S1
|
||||
}])
|
||||
self.owned.execute_operation(self.operation4)
|
||||
self.operation4.refresh_from_db()
|
||||
self.ks4 = RSForm(self.operation4.result)
|
||||
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
|
||||
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
|
||||
self.ks4D2 = self.ks4.insert_last(
|
||||
alias='D2',
|
||||
definition_formal=r'X1 X2 X3 S1 D1',
|
||||
convention='KS4D2'
|
||||
)
|
||||
|
||||
self.operation5 = self.owned.create_operation(
|
||||
alias='5',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
|
||||
self.owned.set_substitutions(self.operation5.pk, [{
|
||||
'original': self.ks4X1,
|
||||
'substitution': self.ks1X2
|
||||
}])
|
||||
self.owned.execute_operation(self.operation5)
|
||||
self.operation5.refresh_from_db()
|
||||
self.ks5 = RSForm(self.operation5.result)
|
||||
self.ks5D4 = self.ks5.insert_last(
|
||||
alias='D4',
|
||||
definition_formal=r'X1 X2 X3 X4 S1 D1 D2 D3',
|
||||
convention='KS5D4'
|
||||
)
|
||||
|
||||
self.operation6 = self.owned.create_operation(
|
||||
alias='6',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
|
||||
self.owned.set_substitutions(self.operation6.pk, [{
|
||||
'original': self.ks2X1,
|
||||
'substitution': self.ks1X1
|
||||
}])
|
||||
self.owned.execute_operation(self.operation6)
|
||||
self.operation6.refresh_from_db()
|
||||
self.ks6 = RSForm(self.operation6.result)
|
||||
self.ks6D2 = self.ks6.insert_last(
|
||||
alias='D2',
|
||||
definition_formal=r'X1 X2 X3 S1 D1',
|
||||
convention='KS6D2'
|
||||
)
|
||||
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation6.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
def test_reference_creation(self):
|
||||
''' Test reference creation. '''
|
||||
self.assertEqual(self.operation1.result, self.operation3.result)
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks2.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 9)
|
||||
self.assertEqual(self.ks6.constituentsQ().count(), 6)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_target_propagation(self):
|
||||
''' Test propagation when deleting a target operation. '''
|
||||
data = {
|
||||
'layout': self.layout_data,
|
||||
'target': self.operation1.pk
|
||||
}
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(self.ks6.constituentsQ().count(), 4)
|
||||
# self.assertEqual(self.ks5.constituentsQ().count(), 5)
|
||||
|
||||
# TODO: add more tests
|
|
@ -23,18 +23,18 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
|
||||
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
|
||||
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||
self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1')
|
||||
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2')
|
||||
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||
|
||||
self.ks2 = RSForm.create(
|
||||
alias='KS2',
|
||||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
|
||||
self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
|
||||
self.ks2S1 = self.ks2.insert_last(
|
||||
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1')
|
||||
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2')
|
||||
self.ks2S1 = self.ks2.insert_new(
|
||||
alias='S1',
|
||||
definition_formal=r'X1',
|
||||
convention='KS2S1'
|
||||
|
@ -45,8 +45,8 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
title='Test3',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks3X1 = self.ks3.insert_last('X1', convention='KS3X1')
|
||||
self.ks3D1 = self.ks3.insert_last(
|
||||
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1')
|
||||
self.ks3D1 = self.ks3.insert_new(
|
||||
alias='D1',
|
||||
definition_formal='X1 X1',
|
||||
convention='KS3D1'
|
||||
|
@ -83,7 +83,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
|
||||
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
|
||||
self.ks4D2 = self.ks4.insert_last(
|
||||
self.ks4D2 = self.ks4.insert_new(
|
||||
alias='D2',
|
||||
definition_formal=r'X1 X2 X3 S1 D1',
|
||||
convention='KS4D2'
|
||||
|
@ -101,7 +101,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.owned.execute_operation(self.operation5)
|
||||
self.operation5.refresh_from_db()
|
||||
self.ks5 = RSForm(self.operation5.result)
|
||||
self.ks5D4 = self.ks5.insert_last(
|
||||
self.ks5D4 = self.ks5.insert_new(
|
||||
alias='D4',
|
||||
definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
|
||||
convention='KS5D4'
|
||||
|
@ -114,17 +114,17 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
{'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
||||
def test_oss_setup(self):
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks2.constituentsQ().count(), 3)
|
||||
self.assertEqual(self.ks3.constituentsQ().count(), 2)
|
||||
self.assertEqual(self.ks4.constituentsQ().count(), 6)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||
self.assertEqual(self.ks1.constituents().count(), 3)
|
||||
self.assertEqual(self.ks2.constituents().count(), 3)
|
||||
self.assertEqual(self.ks3.constituents().count(), 2)
|
||||
self.assertEqual(self.ks4.constituents().count(), 6)
|
||||
self.assertEqual(self.ks5.constituents().count(), 8)
|
||||
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
|
||||
|
||||
|
||||
|
@ -186,7 +186,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
|
||||
|
||||
|
@ -202,7 +202,7 @@ class TestChangeSubstitutions(EndpointTester):
|
|||
self.assertEqual(subs1_2.count(), 0)
|
||||
subs3_4 = self.operation5.getQ_substitutions()
|
||||
self.assertEqual(subs3_4.count(), 1)
|
||||
self.assertEqual(self.ks5.constituentsQ().count(), 7)
|
||||
self.assertEqual(self.ks5.constituents().count(), 7)
|
||||
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
|
||||
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 DEL DEL D1')
|
||||
self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL X3 DEL D1 D2 D3')
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
''' Testing API: Operation Schema - blocks manipulation. '''
|
||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
||||
|
||||
|
@ -56,7 +57,7 @@ class TestOssBlocks(EndpointTester):
|
|||
{'nodeID': 'b' + str(self.block2.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
|
||||
]
|
||||
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
''' Testing API: Operation Schema - operations manipulation. '''
|
||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Reference
|
||||
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||
|
||||
|
@ -24,7 +24,7 @@ class TestOssOperations(EndpointTester):
|
|||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last(
|
||||
self.ks1X1 = self.ks1.insert_new(
|
||||
'X1',
|
||||
term_raw='X1_1',
|
||||
term_resolved='X1_1'
|
||||
|
@ -34,7 +34,7 @@ class TestOssOperations(EndpointTester):
|
|||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last(
|
||||
self.ks2X1 = self.ks2.insert_new(
|
||||
'X2',
|
||||
term_raw='X1_2',
|
||||
term_resolved='X1_2'
|
||||
|
@ -54,17 +54,12 @@ class TestOssOperations(EndpointTester):
|
|||
alias='3',
|
||||
operation_type=OperationType.SYNTHESIS
|
||||
)
|
||||
self.unowned_operation = self.unowned.create_operation(
|
||||
alias='42',
|
||||
operation_type=OperationType.INPUT,
|
||||
result=None
|
||||
)
|
||||
self.layout_data = [
|
||||
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
@ -74,6 +69,7 @@ class TestOssOperations(EndpointTester):
|
|||
'substitution': self.ks2X1
|
||||
}])
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-schema', method='post')
|
||||
def test_create_schema(self):
|
||||
self.populateData()
|
||||
|
@ -127,53 +123,6 @@ class TestOssOperations(EndpointTester):
|
|||
self.executeCreated(data=data, item=self.unowned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/clone-schema', method='post')
|
||||
def test_clone_schema(self):
|
||||
self.populateData()
|
||||
|
||||
data = {
|
||||
'source_operation': self.operation1.pk,
|
||||
'layout': self.layout_data,
|
||||
'position': {
|
||||
'x': 2,
|
||||
'y': 2,
|
||||
'width': 400,
|
||||
'height': 60
|
||||
}
|
||||
}
|
||||
self.executeNotFound(data=data, item=self.invalid_id)
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.assertIn('new_operation', response.data)
|
||||
self.assertIn('oss', response.data)
|
||||
new_operation_id = response.data['new_operation']
|
||||
oss_data = response.data['oss']
|
||||
new_operation = next(op for op in oss_data['operations'] if op['id'] == new_operation_id)
|
||||
self.assertEqual(new_operation['operation_type'], OperationType.INPUT)
|
||||
self.assertTrue(new_operation['alias'].startswith('+'))
|
||||
self.assertTrue(new_operation['title'].startswith('+'))
|
||||
self.assertIsNotNone(new_operation['result'])
|
||||
self.assertEqual(new_operation['parent'], None)
|
||||
|
||||
layout = oss_data['layout']
|
||||
operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0]
|
||||
self.assertEqual(operation_node['x'], data['position']['x'])
|
||||
self.assertEqual(operation_node['y'], data['position']['y'])
|
||||
self.assertEqual(operation_node['width'], data['position']['width'])
|
||||
self.assertEqual(operation_node['height'], data['position']['height'])
|
||||
|
||||
new_schema = LibraryItem.objects.get(pk=new_operation['result'])
|
||||
self.assertEqual(new_schema.alias, new_operation['alias'])
|
||||
self.assertEqual(new_schema.title, new_operation['title'])
|
||||
self.assertEqual(new_schema.description, new_operation['description'])
|
||||
self.assertEqual(self.ks1.constituentsQ().count(), RSForm(new_schema).constituentsQ().count())
|
||||
|
||||
unrelated_data = dict(data)
|
||||
unrelated_data['source_operation'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=unrelated_data, item=self.owned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-schema', method='post')
|
||||
def test_create_schema_parent(self):
|
||||
self.populateData()
|
||||
|
@ -209,37 +158,6 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(new_operation['parent'], block_owned.id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-reference', method='post')
|
||||
def test_create_reference(self):
|
||||
self.populateData()
|
||||
data = {
|
||||
'target': self.invalid_id,
|
||||
'layout': self.layout_data,
|
||||
'position': {
|
||||
'x': 10,
|
||||
'y': 20,
|
||||
'width': 100,
|
||||
'height': 40
|
||||
}
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.model.refresh_from_db()
|
||||
new_operation_id = response.data['new_operation']
|
||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||
self.assertEqual(new_operation['operation_type'], OperationType.REFERENCE)
|
||||
self.assertEqual(new_operation['parent'], self.operation1.parent_id)
|
||||
self.assertEqual(new_operation['result'], self.operation1.result_id)
|
||||
ref = Reference.objects.filter(reference_id=new_operation_id, target_id=self.operation1.pk).first()
|
||||
self.assertIsNotNone(ref)
|
||||
self.assertTrue(Operation.objects.filter(pk=new_operation_id, oss=self.owned.model).exists())
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-synthesis', method='post')
|
||||
def test_create_synthesis(self):
|
||||
self.populateData()
|
||||
|
@ -261,10 +179,10 @@ class TestOssOperations(EndpointTester):
|
|||
'substitutions': []
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.owned.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
new_operation_id = response.data['new_operation']
|
||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||
arguments = Argument.objects.filter(operation__oss=self.owned.model)
|
||||
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))
|
||||
self.assertNotEqual(new_operation['result'], None)
|
||||
|
@ -281,9 +199,6 @@ class TestOssOperations(EndpointTester):
|
|||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
|
@ -298,39 +213,6 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(len(deleted_items), 0)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
|
||||
def test_delete_reference_operation_invalid(self):
|
||||
self.populateData()
|
||||
reference_operation = self.owned.create_reference(self.operation1)
|
||||
data = {
|
||||
'layout': self.layout_data,
|
||||
'target': reference_operation.pk
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/delete-reference', method='patch')
|
||||
def test_delete_reference_operation(self):
|
||||
self.populateData()
|
||||
data = {
|
||||
'layout': self.layout_data,
|
||||
'target': self.invalid_id
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
reference_operation = self.owned.create_reference(self.operation1)
|
||||
self.assertEqual(len(self.operation1.getQ_references()), 1)
|
||||
data['target'] = reference_operation.pk
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = reference_operation.pk
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(len(self.operation1.getQ_references()), 0)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
|
||||
def test_create_input(self):
|
||||
self.populateData()
|
||||
|
@ -366,9 +248,6 @@ class TestOssOperations(EndpointTester):
|
|||
data['target'] = self.operation3.pk
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
|
||||
def test_set_input_null(self):
|
||||
|
@ -396,7 +275,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.ks1.model.alias = 'Test42'
|
||||
self.ks1.model.title = 'Test421'
|
||||
self.ks1.model.description = 'TestComment42'
|
||||
self.ks1.model.save()
|
||||
self.ks1.save()
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.result, self.ks1.model)
|
||||
|
@ -446,7 +325,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
|
||||
ks3x1 = ks3.insert_last('X1', term_resolved='X1_1')
|
||||
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
|
||||
|
||||
data = {
|
||||
'target': self.operation3.pk,
|
||||
|
@ -489,10 +368,6 @@ class TestOssOperations(EndpointTester):
|
|||
data['layout'] = self.layout_data
|
||||
self.executeOK(data=data)
|
||||
|
||||
data_bad = dict(data)
|
||||
data_bad['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data_bad, item=self.owned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_update_operation_sync(self):
|
||||
|
@ -500,7 +375,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.executeBadData(item=self.owned_id)
|
||||
|
||||
data = {
|
||||
'target': self.unowned_operation.pk,
|
||||
'target': self.operation1.pk,
|
||||
'item_data': {
|
||||
'alias': 'Test3 mod',
|
||||
'title': 'Test title mod',
|
||||
|
@ -508,9 +383,7 @@ class TestOssOperations(EndpointTester):
|
|||
},
|
||||
'layout': self.layout_data
|
||||
}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = self.operation1.pk
|
||||
response = self.executeOK(data=data)
|
||||
self.operation1.refresh_from_db()
|
||||
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
|
||||
|
@ -520,17 +393,12 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
|
||||
self.assertEqual(self.operation1.result.description, data['item_data']['description'])
|
||||
|
||||
# Try to update an operation from an unrelated OSS (should fail)
|
||||
data_bad = dict(data)
|
||||
data_bad['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data_bad, item=self.owned_id)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||
def test_update_operation_invalid_substitution(self):
|
||||
self.populateData()
|
||||
|
||||
self.ks1X2 = self.ks1.insert_last('X2')
|
||||
self.ks1X2 = self.ks1.insert_new('X2')
|
||||
|
||||
data = {
|
||||
'target': self.operation3.pk,
|
||||
|
@ -566,9 +434,6 @@ class TestOssOperations(EndpointTester):
|
|||
}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
data['target'] = self.unowned_operation.pk
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
data['target'] = self.operation3.pk
|
||||
self.toggle_admin(True)
|
||||
self.executeBadData(data=data, item=self.unowned_id)
|
||||
|
@ -583,7 +448,7 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(schema.description, self.operation3.description)
|
||||
self.assertEqual(schema.title, self.operation3.title)
|
||||
self.assertEqual(schema.visible, False)
|
||||
items = list(RSForm(schema).constituentsQ())
|
||||
items = list(RSForm(schema).constituents())
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0].alias, 'X1')
|
||||
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
|
||||
|
@ -675,7 +540,6 @@ class TestOssOperations(EndpointTester):
|
|||
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
||||
self.assertEqual(schema.location, self.owned.model.location)
|
||||
|
||||
|
||||
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
||||
def test_import_schema_bad_data(self):
|
||||
self.populateData()
|
||||
|
|
|
@ -25,7 +25,7 @@ class TestOssViewset(EndpointTester):
|
|||
title='Test1',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks1X1 = self.ks1.insert_last(
|
||||
self.ks1X1 = self.ks1.insert_new(
|
||||
'X1',
|
||||
term_raw='X1_1',
|
||||
term_resolved='X1_1'
|
||||
|
@ -35,7 +35,7 @@ class TestOssViewset(EndpointTester):
|
|||
title='Test2',
|
||||
owner=self.user
|
||||
)
|
||||
self.ks2X1 = self.ks2.insert_last(
|
||||
self.ks2X1 = self.ks2.insert_new(
|
||||
'X2',
|
||||
term_raw='X1_2',
|
||||
term_resolved='X1_2'
|
||||
|
@ -60,7 +60,7 @@ class TestOssViewset(EndpointTester):
|
|||
{'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
|
||||
{'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
|
||||
]
|
||||
layout = OperationSchema.layoutQ(self.owned_id)
|
||||
layout = self.owned.layout()
|
||||
layout.data = self.layout_data
|
||||
layout.save()
|
||||
|
||||
|
@ -138,8 +138,8 @@ class TestOssViewset(EndpointTester):
|
|||
|
||||
self.toggle_admin(False)
|
||||
self.executeOK(data=data, item=self.owned_id)
|
||||
self.owned.model.refresh_from_db()
|
||||
self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data'])
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(self.owned.layout().data, data['data'])
|
||||
|
||||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
self.executeForbidden(data=data, item=self.private_id)
|
||||
|
@ -148,7 +148,7 @@ class TestOssViewset(EndpointTester):
|
|||
@decl_endpoint('/api/oss/get-predecessor', method='post')
|
||||
def test_get_predecessor(self):
|
||||
self.populateData()
|
||||
self.ks1X2 = self.ks1.insert_last('X2')
|
||||
self.ks1X2 = self.ks1.insert_new('X2')
|
||||
|
||||
self.owned.execute_operation(self.operation3)
|
||||
self.operation3.refresh_from_db()
|
||||
|
@ -223,13 +223,13 @@ class TestOssViewset(EndpointTester):
|
|||
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||
def test_relocate_constituents(self):
|
||||
self.populateData()
|
||||
self.ks1X2 = self.ks1.insert_last('X2', convention='test')
|
||||
self.ks1X2 = self.ks1.insert_new('X2', convention='test')
|
||||
|
||||
self.owned.execute_operation(self.operation3)
|
||||
self.operation3.refresh_from_db()
|
||||
self.ks3 = RSForm(self.operation3.result)
|
||||
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||
self.ks3X10 = self.ks3.insert_last('X10', convention='test2')
|
||||
self.ks3X10 = self.ks3.insert_new('X10', convention='test2')
|
||||
|
||||
# invalid destination
|
||||
data = {
|
||||
|
|
|
@ -14,7 +14,7 @@ from rest_framework.response import Response
|
|||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from apps.library.serializers import LibraryItemSerializer
|
||||
from apps.rsform.models import Constituenta, RSFormCached
|
||||
from apps.rsform.models import Constituenta, RSForm
|
||||
from apps.rsform.serializers import CstTargetSerializer
|
||||
from shared import messages as msg
|
||||
from shared import permissions
|
||||
|
@ -25,6 +25,7 @@ from .. import serializers as s
|
|||
|
||||
def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem:
|
||||
''' Create clone of prototype schema for operation. '''
|
||||
prototype_schema = RSForm(prototype)
|
||||
clone = deepcopy(prototype)
|
||||
clone.pk = None
|
||||
clone.owner = oss.owner
|
||||
|
@ -36,7 +37,7 @@ def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryIt
|
|||
clone.access_policy = oss.access_policy
|
||||
clone.location = oss.location
|
||||
clone.save()
|
||||
for cst in Constituenta.objects.filter(schema_id=prototype.pk):
|
||||
for cst in prototype_schema.constituents():
|
||||
cst_copy = deepcopy(cst)
|
||||
cst_copy.pk = None
|
||||
cst_copy.schema = clone
|
||||
|
@ -63,13 +64,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'delete_block',
|
||||
'move_items',
|
||||
'create_schema',
|
||||
'clone_schema',
|
||||
'import_schema',
|
||||
'create_reference',
|
||||
'create_synthesis',
|
||||
'update_operation',
|
||||
'delete_operation',
|
||||
'delete_reference',
|
||||
'create_input',
|
||||
'set_input',
|
||||
'execute_operation',
|
||||
|
@ -107,7 +105,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
tags=['OSS'],
|
||||
request=s.LayoutSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||
c.HTTP_200_OK: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
|
@ -117,13 +115,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
''' Endpoint: Update schema layout. '''
|
||||
serializer = s.LayoutSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
item = self._get_item()
|
||||
|
||||
with transaction.atomic():
|
||||
m.Layout.update_data(pk, serializer.validated_data['data'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(item).data)
|
||||
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data['data'])
|
||||
return Response(status=c.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary='create block',
|
||||
|
@ -139,19 +132,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='create-block')
|
||||
def create_block(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create Block. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CreateBlockSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
|
||||
children_operations: list[m.Operation] = serializer.validated_data['children_operations']
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
new_block = oss.create_block(**serializer.validated_data['item_data'])
|
||||
layout.append({
|
||||
'nodeID': 'b' + str(new_block.pk),
|
||||
|
@ -160,7 +152,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'width': position['width'],
|
||||
'height': position['height'],
|
||||
})
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(layout)
|
||||
if len(children_blocks) > 0:
|
||||
for block in children_blocks:
|
||||
block.parent = new_block
|
||||
|
@ -169,13 +161,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
for operation in children_operations:
|
||||
operation.parent = new_block
|
||||
m.Operation.objects.bulk_update(children_operations, ['parent'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_block': new_block.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -193,15 +184,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='update-block')
|
||||
def update_block(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update Block. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UpdateBlockSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
block: m.Block = cast(m.Block, serializer.validated_data['target'])
|
||||
|
||||
block: m.Block = cast(m.Block, serializer.validated_data['target'])
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
if 'layout' in serializer.validated_data:
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
if 'title' in serializer.validated_data['item_data']:
|
||||
block.title = serializer.validated_data['item_data']['title']
|
||||
if 'description' in serializer.validated_data['item_data']:
|
||||
|
@ -209,14 +202,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
if 'parent' in serializer.validated_data['item_data']:
|
||||
block.parent = serializer.validated_data['item_data']['parent']
|
||||
block.save(update_fields=['title', 'description', 'parent'])
|
||||
if 'layout' in serializer.validated_data:
|
||||
layout = serializer.validated_data['layout']
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -233,25 +221,23 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='delete-block')
|
||||
def delete_block(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete Block. '''
|
||||
item = self._get_item()
|
||||
serializer = s.DeleteBlockSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
block = cast(m.Block, serializer.validated_data['target'])
|
||||
layout = serializer.validated_data['layout']
|
||||
layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)]
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
oss.delete_block(block)
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
oss.update_layout(layout)
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -268,27 +254,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='move-items')
|
||||
def move_items(self, request: Request, pk) -> HttpResponse:
|
||||
''' Move items to another parent. '''
|
||||
item = self._get_item()
|
||||
serializer = s.MoveItemsSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
layout = serializer.validated_data['layout']
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
for operation in serializer.validated_data['operations']:
|
||||
operation.parent = serializer.validated_data['destination']
|
||||
operation.save(update_fields=['parent'])
|
||||
for block in serializer.validated_data['blocks']:
|
||||
block.parent = serializer.validated_data['destination']
|
||||
block.save(update_fields=['parent'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -305,19 +289,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='create-schema')
|
||||
def create_schema(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create schema. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CreateSchemaSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
data = serializer.validated_data['item_data']
|
||||
data['operation_type'] = m.OperationType.INPUT
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
|
@ -326,85 +309,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'width': position['width'],
|
||||
'height': position['height']
|
||||
})
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(layout)
|
||||
oss.create_input(new_operation)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': new_operation.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='clone conceptual schema - result of a target operation',
|
||||
tags=['OSS'],
|
||||
request=s.CloneSchemaSerializer(),
|
||||
responses={
|
||||
c.HTTP_201_CREATED: s.OperationCreatedResponse,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='clone-schema')
|
||||
def clone_schema(self, request: Request, pk) -> HttpResponse:
|
||||
''' Clone schema. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CloneSchemaSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
|
||||
with transaction.atomic():
|
||||
source = cast(m.Operation, serializer.validated_data['source_operation'])
|
||||
alias = '+' + source.alias
|
||||
title = '+' + source.title
|
||||
source_schema = cast(LibraryItem, source.result)
|
||||
constituents = Constituenta.objects.filter(schema_id=source_schema.pk)
|
||||
|
||||
new_schema = source_schema
|
||||
new_schema.pk = None
|
||||
new_schema.owner = item.owner
|
||||
new_schema.title = title
|
||||
new_schema.alias = alias
|
||||
new_schema.save()
|
||||
|
||||
for cst in constituents:
|
||||
cst.pk = None
|
||||
cst.schema = new_schema
|
||||
cst.save()
|
||||
|
||||
new_operation = source
|
||||
new_operation.pk = None
|
||||
new_operation.alias = alias
|
||||
new_operation.title = title
|
||||
new_operation.operation_type = m.OperationType.INPUT
|
||||
new_operation.result = None
|
||||
new_operation.save()
|
||||
new_operation.setQ_result(new_schema)
|
||||
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
'x': position['x'],
|
||||
'y': position['y'],
|
||||
'width': position['width'],
|
||||
'height': position['height']
|
||||
})
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': new_operation.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -423,21 +335,20 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='import-schema')
|
||||
def import_schema(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create operation with existing schema. '''
|
||||
item = self._get_item()
|
||||
serializer = s.ImportSchemaSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
data = serializer.validated_data['item_data']
|
||||
data['operation_type'] = m.OperationType.INPUT
|
||||
if not serializer.validated_data['clone_source']:
|
||||
data['result'] = serializer.validated_data['source']
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
|
@ -446,66 +357,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
'width': position['width'],
|
||||
'height': position['height']
|
||||
})
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(layout)
|
||||
|
||||
if serializer.validated_data['clone_source']:
|
||||
prototype: LibraryItem = serializer.validated_data['source']
|
||||
new_operation.result = _create_clone(prototype, new_operation, item)
|
||||
new_operation.result = _create_clone(prototype, new_operation, oss.model)
|
||||
new_operation.save(update_fields=["result"])
|
||||
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': new_operation.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='create reference for operation',
|
||||
tags=['OSS'],
|
||||
request=s.CreateReferenceSerializer(),
|
||||
responses={
|
||||
c.HTTP_201_CREATED: s.OperationCreatedResponse,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['post'], url_path='create-reference')
|
||||
def create_reference(self, request: Request, pk) -> HttpResponse:
|
||||
''' Clone schema. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CreateReferenceSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
target = cast(m.Operation, serializer.validated_data['target'])
|
||||
new_operation = oss.create_reference(target)
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
'x': position['x'],
|
||||
'y': position['y'],
|
||||
'width': position['width'],
|
||||
'height': position['height']
|
||||
})
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': new_operation.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -523,19 +386,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='create-synthesis')
|
||||
def create_synthesis(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create Synthesis operation from arguments. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CreateSynthesisSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
layout = serializer.validated_data['layout']
|
||||
position = serializer.validated_data['position']
|
||||
data = serializer.validated_data['item_data']
|
||||
data['operation_type'] = m.OperationType.SYNTHESIS
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
new_operation = oss.create_operation(**serializer.validated_data['item_data'])
|
||||
layout.append({
|
||||
'nodeID': 'o' + str(new_operation.pk),
|
||||
|
@ -547,14 +409,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss.set_arguments(new_operation.pk, serializer.validated_data['arguments'])
|
||||
oss.set_substitutions(new_operation.pk, serializer.validated_data['substitutions'])
|
||||
oss.execute_operation(new_operation)
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
oss.update_layout(layout)
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_operation': new_operation.pk,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -572,19 +433,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='update-operation')
|
||||
def update_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update Operation arguments and parameters. '''
|
||||
item = self._get_item()
|
||||
serializer = s.UpdateOperationSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
|
||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(item)
|
||||
if 'layout' in serializer.validated_data:
|
||||
layout = serializer.validated_data['layout']
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
if 'alias' in serializer.validated_data['item_data']:
|
||||
operation.alias = serializer.validated_data['item_data']['alias']
|
||||
if 'title' in serializer.validated_data['item_data']:
|
||||
|
@ -606,11 +465,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
|
||||
if 'substitutions' in serializer.validated_data:
|
||||
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -627,68 +484,30 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='delete-operation')
|
||||
def delete_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete Operation. '''
|
||||
item = self._get_item()
|
||||
serializer = s.DeleteOperationSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
old_schema = operation.result
|
||||
layout = serializer.validated_data['layout']
|
||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(item)
|
||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(layout)
|
||||
if old_schema is not None:
|
||||
if serializer.validated_data['delete_schema']:
|
||||
m.PropagationFacade.before_delete_schema(old_schema)
|
||||
old_schema.delete()
|
||||
elif old_schema.is_synced(item):
|
||||
elif old_schema.is_synced(oss.model):
|
||||
old_schema.visible = True
|
||||
old_schema.save(update_fields=['visible'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='delete reference',
|
||||
tags=['OSS'],
|
||||
request=s.DeleteReferenceSerializer(),
|
||||
responses={
|
||||
c.HTTP_200_OK: s.OperationSchemaSerializer,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='delete-reference')
|
||||
def delete_reference(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete Reference Operation. '''
|
||||
item = self._get_item()
|
||||
serializer = s.DeleteReferenceSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
layout = serializer.validated_data['layout']
|
||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(item)
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.delete_reference(operation.pk, serializer.validated_data['keep_connections'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -705,12 +524,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='create-input')
|
||||
def create_input(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create input RSForm. '''
|
||||
item = self._get_item()
|
||||
serializer = s.TargetOperationSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
if len(operation.getQ_arguments()) > 0:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -720,19 +539,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
raise serializers.ValidationError({
|
||||
'target': msg.operationResultNotEmpty(operation.alias)
|
||||
})
|
||||
layout = serializer.validated_data['layout']
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchema(item)
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
schema = oss.create_input(operation)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
'new_schema': LibraryItemSerializer(schema.model).data,
|
||||
'oss': s.OperationSchemaSerializer(item).data
|
||||
'oss': s.OperationSchemaSerializer(oss.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -750,14 +567,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['patch'], url_path='set-input')
|
||||
def set_input(self, request: Request, pk) -> HttpResponse:
|
||||
''' Set input schema for target operation. '''
|
||||
item = self._get_item()
|
||||
serializer = s.SetOperationInputSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
layout = serializer.validated_data['layout']
|
||||
target_operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
schema: Optional[LibraryItem] = serializer.validated_data['input']
|
||||
if schema is not None:
|
||||
|
@ -771,21 +586,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
raise serializers.ValidationError({
|
||||
'input': msg.operationInputAlreadyConnected()
|
||||
})
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
old_schema = target_operation.result
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(item)
|
||||
if old_schema is not None:
|
||||
if old_schema.is_synced(item):
|
||||
if old_schema.is_synced(oss.model):
|
||||
old_schema.visible = True
|
||||
old_schema.save(update_fields=['visible'])
|
||||
m.Layout.update_data(pk, layout)
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
oss.set_input(target_operation.pk, schema)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -802,12 +614,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
@action(detail=True, methods=['post'], url_path='execute-operation')
|
||||
def execute_operation(self, request: Request, pk) -> HttpResponse:
|
||||
''' Execute operation. '''
|
||||
item = self._get_item()
|
||||
serializer = s.TargetOperationSerializer(
|
||||
data=request.data,
|
||||
context={'oss': item}
|
||||
context={'oss': self.get_object()}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||
if operation.operation_type != m.OperationType.SYNTHESIS:
|
||||
raise serializers.ValidationError({
|
||||
|
@ -817,17 +629,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
raise serializers.ValidationError({
|
||||
'target': msg.operationResultNotEmpty(operation.alias)
|
||||
})
|
||||
layout = serializer.validated_data['layout']
|
||||
|
||||
oss = m.OperationSchema(self.get_object())
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(item)
|
||||
oss.update_layout(serializer.validated_data['layout'])
|
||||
oss.execute_operation(operation)
|
||||
m.Layout.update_data(pk, layout)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.OperationSchemaSerializer(item).data
|
||||
data=s.OperationSchemaSerializer(oss.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -879,17 +689,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
|||
''' Relocate constituents from one schema to another. '''
|
||||
serializer = s.RelocateConstituentsSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
ids = [cst.pk for cst in data['items']]
|
||||
oss = m.OperationSchema(LibraryItem.objects.get(pk=data['oss']))
|
||||
source = RSForm(LibraryItem.objects.get(pk=data['source']))
|
||||
destination = RSForm(LibraryItem.objects.get(pk=data['destination']))
|
||||
|
||||
with transaction.atomic():
|
||||
oss = m.OperationSchemaCached(LibraryItem.objects.get(pk=data['oss']))
|
||||
source = RSFormCached(LibraryItem.objects.get(pk=data['source']))
|
||||
destination = RSFormCached(LibraryItem.objects.get(pk=data['destination']))
|
||||
if data['move_down']:
|
||||
oss.relocate_down(source, destination, ids)
|
||||
m.PropagationFacade.before_delete_cst(data['source'], ids)
|
||||
source.delete_cst(ids)
|
||||
oss.relocate_down(source, destination, data['items'])
|
||||
m.PropagationFacade.before_delete_cst(source, data['items'])
|
||||
source.delete_cst(data['items'])
|
||||
else:
|
||||
new_items = oss.relocate_up(source, destination, data['items'])
|
||||
m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk])
|
||||
|
|
|
@ -8,5 +8,5 @@ from . import models
|
|||
class ConstituentaAdmin(admin.ModelAdmin):
|
||||
''' Admin model: Constituenta. '''
|
||||
ordering = ['schema', 'order']
|
||||
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
|
||||
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved']
|
||||
search_fields = ['term_resolved', 'definition_resolved']
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 5.2.4 on 2025-07-29 09:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rsform', '0003_alter_constituenta_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='constituenta',
|
||||
name='crucial',
|
||||
field=models.BooleanField(default=False, verbose_name='Ключевая'),
|
||||
),
|
||||
]
|
|
@ -4,7 +4,6 @@ import re
|
|||
from cctext import extract_entities
|
||||
from django.db.models import (
|
||||
CASCADE,
|
||||
BooleanField,
|
||||
CharField,
|
||||
ForeignKey,
|
||||
JSONField,
|
||||
|
@ -104,10 +103,6 @@ class Constituenta(Model):
|
|||
default='',
|
||||
blank=True
|
||||
)
|
||||
crucial = BooleanField(
|
||||
verbose_name='Ключевая',
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
''' Model metadata. '''
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
''' Models: RSForm order manager. '''
|
||||
|
||||
from .Constituenta import Constituenta, CstType
|
||||
from .RSFormCached import RSFormCached
|
||||
from .SemanticInfo import SemanticInfo
|
||||
|
||||
|
||||
class OrderManager:
|
||||
''' Ordering helper class '''
|
||||
|
||||
def __init__(self, schema: RSFormCached):
|
||||
self._semantic = SemanticInfo(schema)
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
|
||||
def restore_order(self) -> None:
|
||||
''' Implement order restoration process. '''
|
||||
if len(self._items) <= 1:
|
||||
return
|
||||
self._fix_kernel()
|
||||
self._fix_topological()
|
||||
self._fix_semantic_children()
|
||||
self._override_order()
|
||||
|
||||
def _fix_topological(self) -> None:
|
||||
sorted_ids = self._semantic.graph.sort_stable([cst.pk for cst in self._items])
|
||||
sorted_items = [next(cst for cst in self._items if cst.pk == id) for id in sorted_ids]
|
||||
self._items = sorted_items
|
||||
|
||||
def _fix_kernel(self) -> None:
|
||||
result = [cst for cst in self._items if cst.cst_type == CstType.BASE]
|
||||
result = result + [cst for cst in self._items if cst.cst_type == CstType.CONSTANT]
|
||||
kernel = [
|
||||
cst.pk for cst in self._items if
|
||||
cst.cst_type in [CstType.STRUCTURED, CstType.AXIOM] or
|
||||
self._cst_by_ID[self._semantic.parent(cst.pk)].cst_type == CstType.STRUCTURED
|
||||
]
|
||||
kernel = kernel + self._semantic.graph.expand_inputs(kernel)
|
||||
result = result + [cst for cst in self._items if result.count(cst) == 0 and cst.pk in kernel]
|
||||
result = result + [cst for cst in self._items if result.count(cst) == 0]
|
||||
self._items = result
|
||||
|
||||
def _fix_semantic_children(self) -> None:
|
||||
result: list[Constituenta] = []
|
||||
marked: set[Constituenta] = set()
|
||||
for cst in self._items:
|
||||
if cst in marked:
|
||||
continue
|
||||
result.append(cst)
|
||||
children = self._semantic[cst.pk]['children']
|
||||
if len(children) == 0:
|
||||
continue
|
||||
for child in self._items:
|
||||
if child.pk in children:
|
||||
marked.add(child)
|
||||
result.append(child)
|
||||
self._items = result
|
||||
|
||||
def _override_order(self) -> None:
|
||||
order = 0
|
||||
for cst in self._items:
|
||||
cst.order = order
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(self._items, ['order'])
|
|
@ -1,9 +1,8 @@
|
|||
''' Models: RSForm API. '''
|
||||
# pylint: disable=duplicate-code
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Iterable, Optional, cast
|
||||
|
||||
from cctext import Entity, Resolver, TermForm, split_grams
|
||||
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import QuerySet
|
||||
|
||||
|
@ -11,19 +10,28 @@ from apps.library.models import LibraryItem, LibraryItemType, Version
|
|||
from shared import messages as msg
|
||||
|
||||
from ..graph import Graph
|
||||
from .api_RSLanguage import get_type_prefix, guess_type
|
||||
from .Constituenta import Constituenta, CstType, extract_entities, extract_globals
|
||||
from .api_RSLanguage import (
|
||||
generate_structure,
|
||||
get_type_prefix,
|
||||
guess_type,
|
||||
infer_template,
|
||||
is_base_set,
|
||||
is_functional,
|
||||
is_simple_expression,
|
||||
split_template
|
||||
)
|
||||
from .Constituenta import Constituenta, CstType, extract_globals
|
||||
|
||||
INSERT_LAST: int = -1
|
||||
DELETED_ALIAS = 'DEL'
|
||||
|
||||
|
||||
class RSForm:
|
||||
''' RSForm wrapper. No caching, each mutation requires querying. '''
|
||||
''' RSForm is math form of conceptual schema. '''
|
||||
|
||||
def __init__(self, model: LibraryItem):
|
||||
assert model.item_type == LibraryItemType.RSFORM
|
||||
self.model = model
|
||||
self.cache: RSFormCache = RSFormCache(self)
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'RSForm':
|
||||
|
@ -32,11 +40,40 @@ class RSForm:
|
|||
return RSForm(model)
|
||||
|
||||
@staticmethod
|
||||
def resolver_from_schema(schemaID: int) -> Resolver:
|
||||
def from_id(pk: int) -> 'RSForm':
|
||||
''' Get LibraryItem by pk. '''
|
||||
model = LibraryItem.objects.get(pk=pk)
|
||||
return RSForm(model)
|
||||
|
||||
def get_dependant(self, target: Iterable[int]) -> set[int]:
|
||||
''' Get list of constituents depending on target (only 1st degree). '''
|
||||
result: set[int] = set()
|
||||
terms = self._graph_term()
|
||||
formal = self._graph_formal()
|
||||
definitions = self._graph_text()
|
||||
for cst_id in target:
|
||||
result.update(formal.outputs[cst_id])
|
||||
result.update(terms.outputs[cst_id])
|
||||
result.update(definitions.outputs[cst_id])
|
||||
return result
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.save(*args, **kwargs)
|
||||
|
||||
def refresh_from_db(self) -> None:
|
||||
''' Model wrapper. '''
|
||||
self.model.refresh_from_db()
|
||||
self.cache = RSFormCache(self)
|
||||
|
||||
def constituents(self) -> QuerySet[Constituenta]:
|
||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||
return Constituenta.objects.filter(schema=self.model)
|
||||
|
||||
def resolver(self) -> Resolver:
|
||||
''' Create resolver for text references based on schema terms. '''
|
||||
result = Resolver({})
|
||||
constituents = Constituenta.objects.filter(schema_id=schemaID).only('alias', 'term_resolved', 'term_forms')
|
||||
for cst in constituents:
|
||||
for cst in self.constituents().only('alias', 'term_resolved', 'term_forms'):
|
||||
entity = Entity(
|
||||
alias=cst.alias,
|
||||
nominal=cst.term_resolved,
|
||||
|
@ -48,126 +85,23 @@ class RSForm:
|
|||
result.context[cst.alias] = entity
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def resolver_from_list(cst_list: Iterable[Constituenta]) -> Resolver:
|
||||
''' Create resolver for text references based on list of constituents. '''
|
||||
result = Resolver({})
|
||||
for cst in cst_list:
|
||||
entity = Entity(
|
||||
alias=cst.alias,
|
||||
nominal=cst.term_resolved,
|
||||
manual_forms=[
|
||||
TermForm(text=form['text'], grams=split_grams(form['tags']))
|
||||
for form in cst.term_forms
|
||||
]
|
||||
)
|
||||
result.context[cst.alias] = entity
|
||||
return result
|
||||
def semantic(self) -> 'SemanticInfo':
|
||||
''' Access semantic information on constituents. '''
|
||||
return SemanticInfo(self)
|
||||
|
||||
@staticmethod
|
||||
def graph_formal(cst_list: Iterable[Constituenta],
|
||||
cst_by_alias: Optional[dict[str, Constituenta]] = None) -> Graph[int]:
|
||||
''' Graph based on formal definitions. '''
|
||||
result: Graph[int] = Graph()
|
||||
if cst_by_alias is None:
|
||||
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
||||
for cst in cst_list:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for alias in extract_globals(cst.definition_formal):
|
||||
child = cst_by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def graph_term(cst_list: Iterable[Constituenta],
|
||||
cst_by_alias: Optional[dict[str, Constituenta]] = None) -> Graph[int]:
|
||||
''' Graph based on term texts. '''
|
||||
result: Graph[int] = Graph()
|
||||
if cst_by_alias is None:
|
||||
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
||||
for cst in cst_list:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for alias in extract_entities(cst.term_raw):
|
||||
child = cst_by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def graph_text(cst_list: Iterable[Constituenta],
|
||||
cst_by_alias: Optional[Optional[dict[str, Constituenta]]] = None) -> Graph[int]:
|
||||
''' Graph based on definition texts. '''
|
||||
result: Graph[int] = Graph()
|
||||
if cst_by_alias is None:
|
||||
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
||||
for cst in cst_list:
|
||||
result.add_node(cst.pk)
|
||||
for cst in cst_list:
|
||||
for alias in extract_entities(cst.definition_raw):
|
||||
child = cst_by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def save_order(cst_list: Iterable[Constituenta]) -> None:
|
||||
''' Save order for constituents list. '''
|
||||
order = 0
|
||||
changed: list[Constituenta] = []
|
||||
for cst in cst_list:
|
||||
if cst.order != order:
|
||||
cst.order = order
|
||||
changed.append(cst)
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(changed, ['order'])
|
||||
|
||||
@staticmethod
|
||||
def shift_positions(start: int, shift: int, cst_list: list[Constituenta]) -> None:
|
||||
''' Shift positions of constituents. '''
|
||||
if shift == 0:
|
||||
return
|
||||
update_list = cst_list[start:]
|
||||
for cst in update_list:
|
||||
cst.order += shift
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
|
||||
@staticmethod
|
||||
def apply_mapping(mapping: dict[str, str], cst_list: Iterable[Constituenta],
|
||||
change_aliases: bool = False) -> None:
|
||||
''' Apply rename mapping. '''
|
||||
update_list: list[Constituenta] = []
|
||||
for cst in cst_list:
|
||||
if cst.apply_mapping(mapping, change_aliases):
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
|
||||
|
||||
@staticmethod
|
||||
def resolve_term_change(cst_list: Iterable[Constituenta], changed: list[int],
|
||||
cst_by_alias: Optional[Optional[dict[str, Constituenta]]] = None,
|
||||
cst_by_id: Optional[Optional[dict[int, Constituenta]]] = None,
|
||||
resolver: Optional[Resolver] = None) -> None:
|
||||
def after_term_change(self, changed: list[int]) -> None:
|
||||
''' Trigger cascade resolutions when term changes. '''
|
||||
if cst_by_alias is None:
|
||||
cst_by_alias = {cst.alias: cst for cst in cst_list}
|
||||
if cst_by_id is None:
|
||||
cst_by_id = {cst.pk: cst for cst in cst_list}
|
||||
|
||||
graph_terms = RSForm.graph_term(cst_list, cst_by_alias)
|
||||
self.cache.ensure_loaded()
|
||||
graph_terms = self._graph_term()
|
||||
expansion = graph_terms.expand_outputs(changed)
|
||||
expanded_change = changed + expansion
|
||||
update_list: list[Constituenta] = []
|
||||
|
||||
if resolver is None:
|
||||
resolver = RSForm.resolver_from_list(cst_list)
|
||||
|
||||
resolver = self.resolver()
|
||||
if len(expansion) > 0:
|
||||
for cst_id in graph_terms.topological_order():
|
||||
if cst_id not in expansion:
|
||||
continue
|
||||
cst = cst_by_id[cst_id]
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.term_raw)
|
||||
if resolved == resolver.context[cst.alias].get_nominal():
|
||||
continue
|
||||
|
@ -176,34 +110,75 @@ class RSForm:
|
|||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
||||
|
||||
graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
|
||||
graph_defs = self._graph_text()
|
||||
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
|
||||
update_list = []
|
||||
if len(update_defs) == 0:
|
||||
return
|
||||
for cst_id in update_defs:
|
||||
cst = cst_by_id[cst_id]
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.definition_resolved = resolved
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
|
||||
|
||||
def constituentsQ(self) -> QuerySet[Constituenta]:
|
||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||
return Constituenta.objects.filter(schema=self.model)
|
||||
def get_max_index(self, cst_type: str) -> int:
|
||||
''' Get maximum alias index for specific CstType. '''
|
||||
result: int = 0
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = Constituenta.objects \
|
||||
.filter(schema=self.model, cst_type=cst_type) \
|
||||
.only('alias')
|
||||
else:
|
||||
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
|
||||
for cst in cst_list:
|
||||
result = max(result, int(cst.alias[1:]))
|
||||
return result
|
||||
|
||||
def insert_last(
|
||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||
''' Create constituenta from data. '''
|
||||
if insert_after is None:
|
||||
position = INSERT_LAST
|
||||
else:
|
||||
self.cache.ensure_loaded()
|
||||
position = self.cache.constituents.index(self.cache.by_id[insert_after.pk]) + 1
|
||||
result = self.insert_new(data['alias'], data['cst_type'], position)
|
||||
result.convention = data.get('convention', '')
|
||||
result.definition_formal = data.get('definition_formal', '')
|
||||
result.term_forms = data.get('term_forms', [])
|
||||
result.term_raw = data.get('term_raw', '')
|
||||
result.definition_raw = data.get('definition_raw', '')
|
||||
|
||||
if result.term_raw != '' or result.definition_raw != '':
|
||||
resolver = self.resolver()
|
||||
if result.term_raw != '':
|
||||
resolved = resolver.resolve(result.term_raw)
|
||||
result.term_resolved = resolved
|
||||
resolver.context[result.alias] = Entity(result.alias, resolved)
|
||||
if result.definition_raw != '':
|
||||
result.definition_resolved = resolver.resolve(result.definition_raw)
|
||||
|
||||
result.save()
|
||||
self.cache.insert(result)
|
||||
self.after_term_change([result.pk])
|
||||
result.refresh_from_db()
|
||||
return result
|
||||
|
||||
def insert_new(
|
||||
self,
|
||||
alias: str,
|
||||
cst_type: Optional[CstType] = None,
|
||||
position: int = INSERT_LAST,
|
||||
**kwargs
|
||||
) -> Constituenta:
|
||||
''' Insert new constituenta at last position. '''
|
||||
if Constituenta.objects.filter(schema=self.model, alias=alias).exists():
|
||||
''' Insert new constituenta at given position. '''
|
||||
if self.constituents().filter(alias=alias).exists():
|
||||
raise ValidationError(msg.aliasTaken(alias))
|
||||
position = self._get_insert_position(position)
|
||||
if cst_type is None:
|
||||
cst_type = guess_type(alias)
|
||||
position = Constituenta.objects.filter(schema=self.model).count()
|
||||
self._shift_positions(position, 1)
|
||||
result = Constituenta.objects.create(
|
||||
schema=self.model,
|
||||
order=position,
|
||||
|
@ -211,16 +186,115 @@ class RSForm:
|
|||
cst_type=cst_type,
|
||||
**kwargs
|
||||
)
|
||||
self.cache.insert(result)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def insert_copy(
|
||||
self,
|
||||
items: list[Constituenta],
|
||||
position: int = INSERT_LAST,
|
||||
initial_mapping: Optional[dict[str, str]] = None
|
||||
) -> list[Constituenta]:
|
||||
''' Insert copy of target constituents updating references. '''
|
||||
count = len(items)
|
||||
if count == 0:
|
||||
return []
|
||||
|
||||
self.cache.ensure_loaded()
|
||||
position = self._get_insert_position(position)
|
||||
self._shift_positions(position, count)
|
||||
|
||||
indices: dict[str, int] = {}
|
||||
for (value, _) in CstType.choices:
|
||||
indices[value] = -1
|
||||
|
||||
mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
|
||||
for cst in items:
|
||||
if indices[cst.cst_type] == -1:
|
||||
indices[cst.cst_type] = self.get_max_index(cst.cst_type)
|
||||
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
||||
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
||||
mapping[cst.alias] = newAlias
|
||||
|
||||
result = deepcopy(items)
|
||||
for cst in result:
|
||||
cst.pk = None
|
||||
cst.schema = self.model
|
||||
cst.order = position
|
||||
cst.alias = mapping[cst.alias]
|
||||
cst.apply_mapping(mapping)
|
||||
position = position + 1
|
||||
|
||||
new_cst = Constituenta.objects.bulk_create(result)
|
||||
self.cache.insert_multi(new_cst)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update_cst(self, target: Constituenta, data: dict) -> dict:
|
||||
''' Update persistent attributes of a given constituenta. Return old values. '''
|
||||
self.cache.ensure_loaded()
|
||||
cst = self.cache.by_id.get(target.pk)
|
||||
if cst is None:
|
||||
raise ValidationError(msg.constituentaNotInRSform(target.alias))
|
||||
|
||||
old_data = {}
|
||||
term_changed = False
|
||||
if 'convention' in data:
|
||||
if cst.convention == data['convention']:
|
||||
del data['convention']
|
||||
else:
|
||||
old_data['convention'] = cst.convention
|
||||
cst.convention = data['convention']
|
||||
if 'definition_formal' in data:
|
||||
if cst.definition_formal == data['definition_formal']:
|
||||
del data['definition_formal']
|
||||
else:
|
||||
old_data['definition_formal'] = cst.definition_formal
|
||||
cst.definition_formal = data['definition_formal']
|
||||
if 'term_forms' in data:
|
||||
term_changed = True
|
||||
old_data['term_forms'] = cst.term_forms
|
||||
cst.term_forms = data['term_forms']
|
||||
if 'definition_raw' in data or 'term_raw' in data:
|
||||
resolver = self.resolver()
|
||||
if 'term_raw' in data:
|
||||
if cst.term_raw == data['term_raw']:
|
||||
del data['term_raw']
|
||||
else:
|
||||
term_changed = True
|
||||
old_data['term_raw'] = cst.term_raw
|
||||
cst.term_raw = data['term_raw']
|
||||
cst.term_resolved = resolver.resolve(cst.term_raw)
|
||||
if 'term_forms' not in data:
|
||||
cst.term_forms = []
|
||||
resolver.context[cst.alias] = Entity(cst.alias, cst.term_resolved, manual_forms=cst.term_forms)
|
||||
if 'definition_raw' in data:
|
||||
if cst.definition_raw == data['definition_raw']:
|
||||
del data['definition_raw']
|
||||
else:
|
||||
old_data['definition_raw'] = cst.definition_raw
|
||||
cst.definition_raw = data['definition_raw']
|
||||
cst.definition_resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.save()
|
||||
if term_changed:
|
||||
self.after_term_change([cst.pk])
|
||||
self.save(update_fields=['time_update'])
|
||||
return old_data
|
||||
|
||||
def move_cst(self, target: list[Constituenta], destination: int) -> None:
|
||||
''' Move list of constituents to specific position. '''
|
||||
''' Move list of constituents to specific position '''
|
||||
count_moved = 0
|
||||
count_top = 0
|
||||
count_bot = 0
|
||||
size = len(target)
|
||||
|
||||
cst_list = Constituenta.objects.filter(schema=self.model).only('order').order_by('order')
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = self.constituents().only('order').order_by('order')
|
||||
else:
|
||||
cst_list = self.cache.constituents
|
||||
for cst in cst_list:
|
||||
if cst in target:
|
||||
cst.order = destination + count_moved
|
||||
|
@ -232,54 +306,99 @@ class RSForm:
|
|||
cst.order = destination + size + count_bot
|
||||
count_bot += 1
|
||||
Constituenta.objects.bulk_update(cst_list, ['order'])
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def delete_cst(self, target: list[Constituenta]) -> None:
|
||||
''' Delete multiple constituents. '''
|
||||
ids = [cst.pk for cst in target]
|
||||
def delete_cst(self, target: Iterable[Constituenta]) -> None:
|
||||
''' Delete multiple constituents. Do not check if listCst are from this schema. '''
|
||||
mapping = {cst.alias: DELETED_ALIAS for cst in target}
|
||||
Constituenta.objects.filter(pk__in=ids).delete()
|
||||
all_cst = Constituenta.objects.filter(schema=self.model).only(
|
||||
'alias', 'definition_formal', 'term_raw', 'definition_raw', 'order'
|
||||
).order_by('order')
|
||||
RSForm.apply_mapping(mapping, all_cst, change_aliases=False)
|
||||
RSForm.save_order(all_cst)
|
||||
|
||||
def reset_aliases(self) -> None:
|
||||
''' Recreate all aliases based on constituents order. '''
|
||||
bases = cast(dict[str, int], {})
|
||||
mapping = cast(dict[str, str], {})
|
||||
for cst_type in CstType.values:
|
||||
bases[cst_type] = 1
|
||||
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
||||
'alias', 'cst_type', 'definition_formal',
|
||||
'term_raw', 'definition_raw'
|
||||
).order_by('order')
|
||||
for cst in cst_list:
|
||||
alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
|
||||
bases[cst.cst_type] += 1
|
||||
if cst.alias != alias:
|
||||
mapping[cst.alias] = alias
|
||||
RSForm.apply_mapping(mapping, cst_list, change_aliases=True)
|
||||
self.cache.ensure_loaded()
|
||||
self.cache.remove_multi(target)
|
||||
self.apply_mapping(mapping)
|
||||
Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete()
|
||||
self._reset_order()
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
||||
''' Execute constituenta substitution. '''
|
||||
if len(substitutions) < 1:
|
||||
return
|
||||
mapping = {}
|
||||
deleted: list[int] = []
|
||||
replacements: list[int] = []
|
||||
deleted: list[Constituenta] = []
|
||||
replacements: list[Constituenta] = []
|
||||
for original, substitution in substitutions:
|
||||
mapping[original.alias] = substitution.alias
|
||||
deleted.append(original.pk)
|
||||
replacements.append(substitution.pk)
|
||||
Constituenta.objects.filter(pk__in=deleted).delete()
|
||||
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
||||
'alias', 'cst_type', 'definition_formal',
|
||||
'term_raw', 'definition_raw', 'order', 'term_forms', 'term_resolved'
|
||||
).order_by('order')
|
||||
RSForm.save_order(cst_list)
|
||||
RSForm.apply_mapping(mapping, cst_list, change_aliases=False)
|
||||
RSForm.resolve_term_change(cst_list, replacements)
|
||||
deleted.append(original)
|
||||
replacements.append(substitution)
|
||||
self.cache.remove_multi(deleted)
|
||||
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
|
||||
self._reset_order()
|
||||
self.apply_mapping(mapping)
|
||||
self.after_term_change([substitution.pk for substitution in replacements])
|
||||
|
||||
def restore_order(self) -> None:
|
||||
''' Restore order based on types and term graph. '''
|
||||
manager = _OrderManager(self)
|
||||
manager.restore_order()
|
||||
|
||||
def reset_aliases(self) -> None:
|
||||
''' Recreate all aliases based on constituents order. '''
|
||||
mapping = self._create_reset_mapping()
|
||||
self.apply_mapping(mapping, change_aliases=True)
|
||||
|
||||
def change_cst_type(self, target: int, new_type: CstType) -> bool:
|
||||
''' Change type of constituenta generating alias automatically. '''
|
||||
self.cache.ensure_loaded()
|
||||
cst = self.cache.by_id.get(target)
|
||||
if cst is None:
|
||||
return False
|
||||
newAlias = f'{get_type_prefix(new_type)}{self.get_max_index(new_type) + 1}'
|
||||
mapping = {cst.alias: newAlias}
|
||||
cst.cst_type = new_type
|
||||
cst.alias = newAlias
|
||||
cst.save(update_fields=['cst_type', 'alias'])
|
||||
self.apply_mapping(mapping)
|
||||
return True
|
||||
|
||||
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False) -> None:
|
||||
''' Apply rename mapping. '''
|
||||
self.cache.ensure_loaded()
|
||||
update_list: list[Constituenta] = []
|
||||
for cst in self.cache.constituents:
|
||||
if cst.apply_mapping(mapping, change_aliases):
|
||||
update_list.append(cst)
|
||||
if change_aliases:
|
||||
self.cache.reset_aliases()
|
||||
Constituenta.objects.bulk_update(update_list, ['alias', 'definition_formal', 'term_raw', 'definition_raw'])
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def apply_partial_mapping(self, mapping: dict[str, str], target: list[int]) -> None:
|
||||
''' Apply rename mapping to target constituents. '''
|
||||
self.cache.ensure_loaded()
|
||||
update_list: list[Constituenta] = []
|
||||
for cst in self.cache.constituents:
|
||||
if cst.pk in target:
|
||||
if cst.apply_mapping(mapping):
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['definition_formal', 'term_raw', 'definition_raw'])
|
||||
self.save(update_fields=['time_update'])
|
||||
|
||||
def resolve_all_text(self) -> None:
|
||||
''' Trigger reference resolution for all texts. '''
|
||||
self.cache.ensure_loaded()
|
||||
graph_terms = self._graph_term()
|
||||
resolver = Resolver({})
|
||||
update_list: list[Constituenta] = []
|
||||
for cst_id in graph_terms.topological_order():
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.term_raw)
|
||||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||
cst.term_resolved = resolved
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
||||
|
||||
for cst in self.cache.constituents:
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.definition_resolved = resolved
|
||||
Constituenta.objects.bulk_update(self.cache.constituents, ['definition_resolved'])
|
||||
|
||||
|
||||
def create_version(self, version: str, description: str, data) -> Version:
|
||||
''' Creates version for current state. '''
|
||||
|
@ -289,3 +408,370 @@ class RSForm:
|
|||
description=description,
|
||||
data=data
|
||||
)
|
||||
|
||||
def produce_structure(self, target: Constituenta, parse: dict) -> list[Constituenta]:
|
||||
''' Add constituents for each structural element of the target. '''
|
||||
expressions = generate_structure(
|
||||
alias=target.alias,
|
||||
expression=target.definition_formal,
|
||||
parse=parse
|
||||
)
|
||||
count_new = len(expressions)
|
||||
if count_new == 0:
|
||||
return []
|
||||
|
||||
self.cache.ensure_loaded()
|
||||
position = self.cache.constituents.index(self.cache.by_id[target.id]) + 1
|
||||
self._shift_positions(position, count_new)
|
||||
result = []
|
||||
cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION
|
||||
free_index = self.get_max_index(cst_type) + 1
|
||||
prefix = get_type_prefix(cst_type)
|
||||
for text in expressions:
|
||||
new_item = Constituenta.objects.create(
|
||||
schema=self.model,
|
||||
order=position,
|
||||
alias=f'{prefix}{free_index}',
|
||||
definition_formal=text,
|
||||
cst_type=cst_type
|
||||
)
|
||||
result.append(new_item)
|
||||
free_index = free_index + 1
|
||||
position = position + 1
|
||||
|
||||
self.cache.insert_multi(result)
|
||||
self.save(update_fields=['time_update'])
|
||||
return result
|
||||
|
||||
def _create_reset_mapping(self) -> dict[str, str]:
|
||||
bases = cast(dict[str, int], {})
|
||||
mapping = cast(dict[str, str], {})
|
||||
for cst_type in CstType.values:
|
||||
bases[cst_type] = 1
|
||||
cst_list = self.constituents().order_by('order')
|
||||
for cst in cst_list:
|
||||
alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
|
||||
bases[cst.cst_type] += 1
|
||||
if cst.alias != alias:
|
||||
mapping[cst.alias] = alias
|
||||
return mapping
|
||||
|
||||
def _shift_positions(self, start: int, shift: int) -> None:
|
||||
if shift == 0:
|
||||
return
|
||||
self.cache.ensure_loaded()
|
||||
update_list = self.cache.constituents[start:]
|
||||
for cst in update_list:
|
||||
cst.order += shift
|
||||
Constituenta.objects.bulk_update(update_list, ['order'])
|
||||
|
||||
def _get_insert_position(self, position: int) -> int:
|
||||
if position < 0 and position != INSERT_LAST:
|
||||
raise ValidationError(msg.invalidPosition())
|
||||
lastPosition = self.constituents().count()
|
||||
if position == INSERT_LAST:
|
||||
return lastPosition
|
||||
else:
|
||||
return max(0, min(position, lastPosition))
|
||||
|
||||
def _reset_order(self) -> None:
|
||||
order = 0
|
||||
changed: list[Constituenta] = []
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = self.constituents().only('order').order_by('order')
|
||||
else:
|
||||
cst_list = self.cache.constituents
|
||||
for cst in cst_list:
|
||||
if cst.order != order:
|
||||
cst.order = order
|
||||
changed.append(cst)
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(changed, ['order'])
|
||||
|
||||
def _graph_formal(self) -> Graph[int]:
|
||||
''' Graph based on formal definitions. '''
|
||||
self.cache.ensure_loaded()
|
||||
result: Graph[int] = Graph()
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_globals(cst.definition_formal):
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
def _graph_term(self) -> Graph[int]:
|
||||
''' Graph based on term texts. '''
|
||||
self.cache.ensure_loaded()
|
||||
result: Graph[int] = Graph()
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_entities(cst.term_raw):
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
def _graph_text(self) -> Graph[int]:
|
||||
''' Graph based on definition texts. '''
|
||||
self.cache.ensure_loaded()
|
||||
result: Graph[int] = Graph()
|
||||
for cst in self.cache.constituents:
|
||||
result.add_node(cst.pk)
|
||||
for cst in self.cache.constituents:
|
||||
for alias in extract_entities(cst.definition_raw):
|
||||
child = self.cache.by_alias.get(alias)
|
||||
if child is not None:
|
||||
result.add_edge(src=child.pk, dest=cst.pk)
|
||||
return result
|
||||
|
||||
|
||||
class RSFormCache:
|
||||
''' Cache for RSForm constituents. '''
|
||||
|
||||
def __init__(self, schema: 'RSForm'):
|
||||
self._schema = schema
|
||||
self.constituents: list[Constituenta] = []
|
||||
self.by_id: dict[int, Constituenta] = {}
|
||||
self.by_alias: dict[str, Constituenta] = {}
|
||||
self.is_loaded = False
|
||||
|
||||
def reload(self) -> None:
|
||||
self.constituents = list(
|
||||
self._schema.constituents().only(
|
||||
'order',
|
||||
'alias',
|
||||
'cst_type',
|
||||
'definition_formal',
|
||||
'term_raw',
|
||||
'definition_raw'
|
||||
).order_by('order')
|
||||
)
|
||||
self.by_id = {cst.pk: cst for cst in self.constituents}
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
self.is_loaded = True
|
||||
|
||||
def ensure_loaded(self) -> None:
|
||||
if not self.is_loaded:
|
||||
self.reload()
|
||||
|
||||
def reset_aliases(self) -> None:
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
|
||||
def clear(self) -> None:
|
||||
self.constituents = []
|
||||
self.by_id = {}
|
||||
self.by_alias = {}
|
||||
self.is_loaded = False
|
||||
|
||||
def insert(self, cst: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.insert(cst.order, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def insert_multi(self, items: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in items:
|
||||
self.constituents.insert(cst.order, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def remove(self, target: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.remove(self.by_id[target.pk])
|
||||
del self.by_id[target.pk]
|
||||
del self.by_alias[target.alias]
|
||||
|
||||
def remove_multi(self, target: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in target:
|
||||
self.constituents.remove(self.by_id[cst.pk])
|
||||
del self.by_id[cst.pk]
|
||||
del self.by_alias[cst.alias]
|
||||
|
||||
|
||||
class SemanticInfo:
|
||||
''' Semantic information derived from constituents. '''
|
||||
|
||||
def __init__(self, schema: RSForm):
|
||||
schema.cache.ensure_loaded()
|
||||
self._graph = schema._graph_formal()
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
self._cst_by_alias = schema.cache.by_alias
|
||||
self.info = {
|
||||
cst.pk: {
|
||||
'is_simple': False,
|
||||
'is_template': False,
|
||||
'parent': cst.pk,
|
||||
'children': []
|
||||
}
|
||||
for cst in schema.cache.constituents
|
||||
}
|
||||
self._calculate_attributes()
|
||||
|
||||
def __getitem__(self, key: int) -> dict:
|
||||
return self.info[key]
|
||||
|
||||
def is_simple_expression(self, target: int) -> bool:
|
||||
''' Access "is_simple" attribute. '''
|
||||
return cast(bool, self.info[target]['is_simple'])
|
||||
|
||||
def is_template(self, target: int) -> bool:
|
||||
''' Access "is_template" attribute. '''
|
||||
return cast(bool, self.info[target]['is_template'])
|
||||
|
||||
def parent(self, target: int) -> int:
|
||||
''' Access "parent" attribute. '''
|
||||
return cast(int, self.info[target]['parent'])
|
||||
|
||||
def children(self, target: int) -> list[int]:
|
||||
''' Access "children" attribute. '''
|
||||
return cast(list[int], self.info[target]['children'])
|
||||
|
||||
def _calculate_attributes(self) -> None:
|
||||
for cst_id in self._graph.topological_order():
|
||||
cst = self._cst_by_ID[cst_id]
|
||||
self.info[cst_id]['is_template'] = infer_template(cst.definition_formal)
|
||||
self.info[cst_id]['is_simple'] = self._infer_simple_expression(cst)
|
||||
if not self.info[cst_id]['is_simple'] or cst.cst_type == CstType.STRUCTURED:
|
||||
continue
|
||||
parent = self._infer_parent(cst)
|
||||
self.info[cst_id]['parent'] = parent
|
||||
if parent != cst_id:
|
||||
cast(list[int], self.info[parent]['children']).append(cst_id)
|
||||
|
||||
def _infer_simple_expression(self, target: Constituenta) -> bool:
|
||||
if target.cst_type == CstType.STRUCTURED or is_base_set(target.cst_type):
|
||||
return False
|
||||
|
||||
dependencies = self._graph.inputs[target.pk]
|
||||
has_complex_dependency = any(
|
||||
self.is_template(cst_id) and
|
||||
not self.is_simple_expression(cst_id) for cst_id in dependencies
|
||||
)
|
||||
if has_complex_dependency:
|
||||
return False
|
||||
|
||||
if is_functional(target.cst_type):
|
||||
return is_simple_expression(split_template(target.definition_formal)['body'])
|
||||
else:
|
||||
return is_simple_expression(target.definition_formal)
|
||||
|
||||
def _infer_parent(self, target: Constituenta) -> int:
|
||||
sources = self._extract_sources(target)
|
||||
if len(sources) != 1:
|
||||
return target.pk
|
||||
|
||||
parent_id = next(iter(sources))
|
||||
parent = self._cst_by_ID[parent_id]
|
||||
if is_base_set(parent.cst_type):
|
||||
return target.pk
|
||||
return parent_id
|
||||
|
||||
def _extract_sources(self, target: Constituenta) -> set[int]:
|
||||
sources: set[int] = set()
|
||||
if not is_functional(target.cst_type):
|
||||
for parent_id in self._graph.inputs[target.pk]:
|
||||
parent_info = self[parent_id]
|
||||
if not parent_info['is_template'] or not parent_info['is_simple']:
|
||||
sources.add(parent_info['parent'])
|
||||
return sources
|
||||
|
||||
expression = split_template(target.definition_formal)
|
||||
body_dependencies = extract_globals(expression['body'])
|
||||
for alias in body_dependencies:
|
||||
parent = self._cst_by_alias.get(alias)
|
||||
if not parent:
|
||||
continue
|
||||
|
||||
parent_info = self[parent.pk]
|
||||
if not parent_info['is_template'] or not parent_info['is_simple']:
|
||||
sources.add(parent_info['parent'])
|
||||
|
||||
if self._need_check_head(sources, expression['head']):
|
||||
head_dependencies = extract_globals(expression['head'])
|
||||
for alias in head_dependencies:
|
||||
parent = self._cst_by_alias.get(alias)
|
||||
if not parent:
|
||||
continue
|
||||
|
||||
parent_info = self[parent.pk]
|
||||
if not is_base_set(parent.cst_type) and \
|
||||
(not parent_info['is_template'] or not parent_info['is_simple']):
|
||||
sources.add(parent_info['parent'])
|
||||
return sources
|
||||
|
||||
def _need_check_head(self, sources: set[int], head: str) -> bool:
|
||||
if len(sources) == 0:
|
||||
return True
|
||||
elif len(sources) != 1:
|
||||
return False
|
||||
else:
|
||||
base = self._cst_by_ID[next(iter(sources))]
|
||||
return not is_functional(base.cst_type) or \
|
||||
split_template(base.definition_formal)['head'] != head
|
||||
|
||||
|
||||
class _OrderManager:
|
||||
''' Ordering helper class '''
|
||||
|
||||
def __init__(self, schema: RSForm):
|
||||
self._semantic = schema.semantic()
|
||||
self._graph = schema._graph_formal()
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
|
||||
def restore_order(self) -> None:
|
||||
''' Implement order restoration process. '''
|
||||
if len(self._items) <= 1:
|
||||
return
|
||||
self._fix_kernel()
|
||||
self._fix_topological()
|
||||
self._fix_semantic_children()
|
||||
self._save_order()
|
||||
|
||||
def _fix_topological(self) -> None:
|
||||
sorted_ids = self._graph.sort_stable([cst.pk for cst in self._items])
|
||||
sorted_items = [next(cst for cst in self._items if cst.pk == id) for id in sorted_ids]
|
||||
self._items = sorted_items
|
||||
|
||||
def _fix_kernel(self) -> None:
|
||||
result = [cst for cst in self._items if cst.cst_type == CstType.BASE]
|
||||
result = result + [cst for cst in self._items if cst.cst_type == CstType.CONSTANT]
|
||||
kernel = [
|
||||
cst.pk for cst in self._items if
|
||||
cst.cst_type in [CstType.STRUCTURED, CstType.AXIOM] or
|
||||
self._cst_by_ID[self._semantic.parent(cst.pk)].cst_type == CstType.STRUCTURED
|
||||
]
|
||||
kernel = kernel + self._graph.expand_inputs(kernel)
|
||||
result = result + [cst for cst in self._items if result.count(cst) == 0 and cst.pk in kernel]
|
||||
result = result + [cst for cst in self._items if result.count(cst) == 0]
|
||||
self._items = result
|
||||
|
||||
def _fix_semantic_children(self) -> None:
|
||||
result: list[Constituenta] = []
|
||||
marked: set[Constituenta] = set()
|
||||
for cst in self._items:
|
||||
if cst in marked:
|
||||
continue
|
||||
result.append(cst)
|
||||
children = self._semantic[cst.pk]['children']
|
||||
if len(children) == 0:
|
||||
continue
|
||||
for child in self._items:
|
||||
if child.pk in children:
|
||||
marked.add(child)
|
||||
result.append(child)
|
||||
self._items = result
|
||||
|
||||
def _save_order(self) -> None:
|
||||
order = 0
|
||||
for cst in self._items:
|
||||
cst.order = order
|
||||
order += 1
|
||||
Constituenta.objects.bulk_update(self._items, ['order'])
|
||||
|
|
|
@ -1,438 +0,0 @@
|
|||
''' Models: RSForm API. '''
|
||||
# pylint: disable=duplicate-code
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Iterable, Optional, cast
|
||||
|
||||
from cctext import Entity, Resolver
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from apps.library.models import LibraryItem, LibraryItemType
|
||||
from shared import messages as msg
|
||||
|
||||
from .api_RSLanguage import generate_structure, get_type_prefix, guess_type
|
||||
from .Constituenta import Constituenta, CstType
|
||||
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
||||
|
||||
|
||||
class RSFormCached:
|
||||
''' RSForm cached. Caching allows to avoid querying for each method call. '''
|
||||
|
||||
def __init__(self, model: LibraryItem):
|
||||
self.model = model
|
||||
self.cache: _RSFormCache = _RSFormCache(self)
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs) -> 'RSFormCached':
|
||||
''' Create LibraryItem via RSForm. '''
|
||||
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
|
||||
return RSFormCached(model)
|
||||
|
||||
@staticmethod
|
||||
def from_id(pk: int) -> 'RSFormCached':
|
||||
''' Get LibraryItem by pk. '''
|
||||
model = LibraryItem.objects.get(pk=pk)
|
||||
return RSFormCached(model)
|
||||
|
||||
def get_dependant(self, target: Iterable[int]) -> set[int]:
|
||||
''' Get list of constituents depending on target (only 1st degree). '''
|
||||
self.cache.ensure_loaded()
|
||||
result: set[int] = set()
|
||||
terms = RSForm.graph_term(self.cache.constituents, self.cache.by_alias)
|
||||
formal = RSForm.graph_formal(self.cache.constituents, self.cache.by_alias)
|
||||
definitions = RSForm.graph_text(self.cache.constituents, self.cache.by_alias)
|
||||
for cst_id in target:
|
||||
result.update(formal.outputs[cst_id])
|
||||
result.update(terms.outputs[cst_id])
|
||||
result.update(definitions.outputs[cst_id])
|
||||
return result
|
||||
|
||||
def constituentsQ(self) -> QuerySet[Constituenta]:
|
||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||
return Constituenta.objects.filter(schema=self.model)
|
||||
|
||||
def insert_last(
|
||||
self,
|
||||
alias: str,
|
||||
cst_type: Optional[CstType] = None,
|
||||
**kwargs
|
||||
) -> Constituenta:
|
||||
''' Insert new constituenta at last position. '''
|
||||
if cst_type is None:
|
||||
cst_type = guess_type(alias)
|
||||
position = Constituenta.objects.filter(schema=self.model).count()
|
||||
result = Constituenta.objects.create(
|
||||
schema=self.model,
|
||||
order=position,
|
||||
alias=alias,
|
||||
cst_type=cst_type,
|
||||
**kwargs
|
||||
)
|
||||
self.cache.is_loaded = False
|
||||
return result
|
||||
|
||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||
''' Create constituenta from data. '''
|
||||
self.cache.ensure_loaded_terms()
|
||||
if insert_after is not None:
|
||||
position = self.cache.by_id[insert_after.pk].order + 1
|
||||
else:
|
||||
position = len(self.cache.constituents)
|
||||
RSForm.shift_positions(position, 1, self.cache.constituents)
|
||||
|
||||
result = Constituenta.objects.create(
|
||||
schema=self.model,
|
||||
order=position,
|
||||
alias=data['alias'],
|
||||
cst_type=data['cst_type'],
|
||||
crucial=data.get('crucial', False),
|
||||
convention=data.get('convention', ''),
|
||||
definition_formal=data.get('definition_formal', ''),
|
||||
term_forms=data.get('term_forms', []),
|
||||
term_raw=data.get('term_raw', ''),
|
||||
definition_raw=data.get('definition_raw', '')
|
||||
)
|
||||
|
||||
if result.term_raw != '' or result.definition_raw != '':
|
||||
resolver = RSForm.resolver_from_list(self.cache.constituents)
|
||||
if result.term_raw != '':
|
||||
resolved = resolver.resolve(result.term_raw)
|
||||
result.term_resolved = resolved
|
||||
resolver.context[result.alias] = Entity(result.alias, resolved)
|
||||
if result.definition_raw != '':
|
||||
result.definition_resolved = resolver.resolve(result.definition_raw)
|
||||
|
||||
result.save()
|
||||
self.cache.insert(result)
|
||||
RSForm.resolve_term_change(self.cache.constituents, [result.pk], self.cache.by_alias, self.cache.by_id)
|
||||
return result
|
||||
|
||||
def insert_copy(
|
||||
self,
|
||||
items: list[Constituenta],
|
||||
position: int = INSERT_LAST,
|
||||
initial_mapping: Optional[dict[str, str]] = None
|
||||
) -> list[Constituenta]:
|
||||
''' Insert copy of target constituents updating references. '''
|
||||
count = len(items)
|
||||
if count == 0:
|
||||
return []
|
||||
|
||||
self.cache.ensure_loaded()
|
||||
lastPosition = len(self.cache.constituents)
|
||||
if position == INSERT_LAST:
|
||||
position = lastPosition
|
||||
else:
|
||||
position = max(0, min(position, lastPosition))
|
||||
RSForm.shift_positions(position, count, self.cache.constituents)
|
||||
|
||||
indices: dict[str, int] = {}
|
||||
for (value, _) in CstType.choices:
|
||||
indices[value] = -1
|
||||
|
||||
mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
|
||||
for cst in items:
|
||||
if indices[cst.cst_type] == -1:
|
||||
indices[cst.cst_type] = self._get_max_index(cst.cst_type)
|
||||
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
||||
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
||||
mapping[cst.alias] = newAlias
|
||||
|
||||
result = deepcopy(items)
|
||||
for cst in result:
|
||||
cst.pk = None
|
||||
cst.schema = self.model
|
||||
cst.order = position
|
||||
cst.alias = mapping[cst.alias]
|
||||
cst.apply_mapping(mapping)
|
||||
position = position + 1
|
||||
|
||||
new_cst = Constituenta.objects.bulk_create(result)
|
||||
self.cache.insert_multi(new_cst)
|
||||
return result
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update_cst(self, target: int, data: dict) -> dict:
|
||||
''' Update persistent attributes of a given constituenta. Return old values. '''
|
||||
self.cache.ensure_loaded_terms()
|
||||
cst = self.cache.by_id.get(target)
|
||||
if cst is None:
|
||||
raise ValidationError(msg.constituentaNotInRSform(str(target)))
|
||||
|
||||
old_data = {}
|
||||
term_changed = False
|
||||
if 'convention' in data:
|
||||
if cst.convention == data['convention']:
|
||||
del data['convention']
|
||||
else:
|
||||
old_data['convention'] = cst.convention
|
||||
cst.convention = data['convention']
|
||||
if 'crucial' in data:
|
||||
cst.crucial = data['crucial']
|
||||
del data['crucial']
|
||||
if 'definition_formal' in data:
|
||||
if cst.definition_formal == data['definition_formal']:
|
||||
del data['definition_formal']
|
||||
else:
|
||||
old_data['definition_formal'] = cst.definition_formal
|
||||
cst.definition_formal = data['definition_formal']
|
||||
if 'term_forms' in data:
|
||||
term_changed = True
|
||||
old_data['term_forms'] = cst.term_forms
|
||||
cst.term_forms = data['term_forms']
|
||||
|
||||
resolver: Optional[Resolver] = None
|
||||
if 'definition_raw' in data or 'term_raw' in data:
|
||||
resolver = RSForm.resolver_from_list(self.cache.constituents)
|
||||
if 'term_raw' in data:
|
||||
if cst.term_raw == data['term_raw']:
|
||||
del data['term_raw']
|
||||
else:
|
||||
term_changed = True
|
||||
old_data['term_raw'] = cst.term_raw
|
||||
cst.term_raw = data['term_raw']
|
||||
cst.term_resolved = resolver.resolve(cst.term_raw)
|
||||
if 'term_forms' not in data:
|
||||
cst.term_forms = []
|
||||
resolver.context[cst.alias] = Entity(cst.alias, cst.term_resolved, manual_forms=cst.term_forms)
|
||||
if 'definition_raw' in data:
|
||||
if cst.definition_raw == data['definition_raw']:
|
||||
del data['definition_raw']
|
||||
else:
|
||||
old_data['definition_raw'] = cst.definition_raw
|
||||
cst.definition_raw = data['definition_raw']
|
||||
cst.definition_resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.save()
|
||||
if term_changed:
|
||||
RSForm.resolve_term_change(
|
||||
self.cache.constituents, [cst.pk],
|
||||
self.cache.by_alias, self.cache.by_id, resolver
|
||||
)
|
||||
return old_data
|
||||
|
||||
def delete_cst(self, target: list[int]) -> None:
|
||||
''' Delete multiple constituents. '''
|
||||
self.cache.ensure_loaded()
|
||||
cst_list = [self.cache.by_id[cst_id] for cst_id in target]
|
||||
mapping = {cst.alias: DELETED_ALIAS for cst in cst_list}
|
||||
self.cache.remove_multi(cst_list)
|
||||
self.apply_mapping(mapping)
|
||||
Constituenta.objects.filter(pk__in=target).delete()
|
||||
RSForm.save_order(self.cache.constituents)
|
||||
|
||||
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
|
||||
''' Execute constituenta substitution. '''
|
||||
if len(substitutions) < 1:
|
||||
return
|
||||
self.cache.ensure_loaded_terms()
|
||||
mapping = {}
|
||||
deleted: list[Constituenta] = []
|
||||
replacements: list[int] = []
|
||||
for original, substitution in substitutions:
|
||||
mapping[original.alias] = substitution.alias
|
||||
deleted.append(original)
|
||||
replacements.append(substitution.pk)
|
||||
self.cache.remove_multi(deleted)
|
||||
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
|
||||
RSForm.save_order(self.cache.constituents)
|
||||
self.apply_mapping(mapping)
|
||||
RSForm.resolve_term_change(self.cache.constituents, replacements, self.cache.by_alias, self.cache.by_id)
|
||||
|
||||
def reset_aliases(self) -> None:
|
||||
''' Recreate all aliases based on constituents order. '''
|
||||
self.cache.ensure_loaded()
|
||||
bases = cast(dict[str, int], {})
|
||||
mapping = cast(dict[str, str], {})
|
||||
for cst_type in CstType.values:
|
||||
bases[cst_type] = 1
|
||||
for cst in self.cache.constituents:
|
||||
alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
|
||||
bases[cst.cst_type] += 1
|
||||
if cst.alias != alias:
|
||||
mapping[cst.alias] = alias
|
||||
self.apply_mapping(mapping, change_aliases=True)
|
||||
|
||||
def change_cst_type(self, target: int, new_type: CstType) -> bool:
|
||||
''' Change type of constituenta generating alias automatically. '''
|
||||
self.cache.ensure_loaded()
|
||||
cst = self.cache.by_id.get(target)
|
||||
if cst is None:
|
||||
return False
|
||||
newAlias = f'{get_type_prefix(new_type)}{self._get_max_index(new_type) + 1}'
|
||||
mapping = {cst.alias: newAlias}
|
||||
cst.cst_type = new_type
|
||||
cst.alias = newAlias
|
||||
cst.save(update_fields=['cst_type', 'alias'])
|
||||
self.apply_mapping(mapping)
|
||||
return True
|
||||
|
||||
def apply_mapping(self, mapping: dict[str, str], change_aliases: bool = False) -> None:
|
||||
''' Apply rename mapping. '''
|
||||
self.cache.ensure_loaded()
|
||||
RSForm.apply_mapping(mapping, self.cache.constituents, change_aliases)
|
||||
if change_aliases:
|
||||
self.cache.reload_aliases()
|
||||
|
||||
def apply_partial_mapping(self, mapping: dict[str, str], target: list[int]) -> None:
|
||||
''' Apply rename mapping to target constituents. '''
|
||||
self.cache.ensure_loaded()
|
||||
update_list: list[Constituenta] = []
|
||||
for cst in self.cache.constituents:
|
||||
if cst.pk in target:
|
||||
if cst.apply_mapping(mapping):
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['definition_formal', 'term_raw', 'definition_raw'])
|
||||
|
||||
def resolve_all_text(self) -> None:
|
||||
''' Trigger reference resolution for all texts. '''
|
||||
self.cache.ensure_loaded()
|
||||
graph_terms = RSForm.graph_term(self.cache.constituents, self.cache.by_alias)
|
||||
resolver = Resolver({})
|
||||
update_list: list[Constituenta] = []
|
||||
for cst_id in graph_terms.topological_order():
|
||||
cst = self.cache.by_id[cst_id]
|
||||
resolved = resolver.resolve(cst.term_raw)
|
||||
resolver.context[cst.alias] = Entity(cst.alias, resolved)
|
||||
cst.term_resolved = resolved
|
||||
update_list.append(cst)
|
||||
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
|
||||
|
||||
for cst in self.cache.constituents:
|
||||
resolved = resolver.resolve(cst.definition_raw)
|
||||
cst.definition_resolved = resolved
|
||||
Constituenta.objects.bulk_update(self.cache.constituents, ['definition_resolved'])
|
||||
|
||||
|
||||
def produce_structure(self, target: Constituenta, parse: dict) -> list[Constituenta]:
|
||||
''' Add constituents for each structural element of the target. '''
|
||||
expressions = generate_structure(
|
||||
alias=target.alias,
|
||||
expression=target.definition_formal,
|
||||
parse=parse
|
||||
)
|
||||
count_new = len(expressions)
|
||||
if count_new == 0:
|
||||
return []
|
||||
|
||||
self.cache.ensure_loaded()
|
||||
position = self.cache.constituents.index(self.cache.by_id[target.id]) + 1
|
||||
RSForm.shift_positions(position, count_new, self.cache.constituents)
|
||||
|
||||
result = []
|
||||
cst_type = CstType.TERM if len(parse['args']) == 0 else CstType.FUNCTION
|
||||
free_index = self._get_max_index(cst_type) + 1
|
||||
prefix = get_type_prefix(cst_type)
|
||||
for text in expressions:
|
||||
new_item = Constituenta.objects.create(
|
||||
schema=self.model,
|
||||
order=position,
|
||||
alias=f'{prefix}{free_index}',
|
||||
definition_formal=text,
|
||||
cst_type=cst_type
|
||||
)
|
||||
result.append(new_item)
|
||||
free_index = free_index + 1
|
||||
position = position + 1
|
||||
|
||||
self.cache.insert_multi(result)
|
||||
return result
|
||||
|
||||
def _get_max_index(self, cst_type: str) -> int:
|
||||
''' Get maximum alias index for specific CstType. '''
|
||||
cst_list: Iterable[Constituenta] = []
|
||||
if not self.cache.is_loaded:
|
||||
cst_list = Constituenta.objects \
|
||||
.filter(schema=self.model, cst_type=cst_type) \
|
||||
.only('alias')
|
||||
else:
|
||||
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
|
||||
|
||||
result: int = 0
|
||||
for cst in cst_list:
|
||||
result = max(result, int(cst.alias[1:]))
|
||||
return result
|
||||
|
||||
|
||||
class _RSFormCache:
|
||||
''' Cache for RSForm constituents. '''
|
||||
|
||||
def __init__(self, schema: 'RSFormCached'):
|
||||
self._schema = schema
|
||||
self.constituents: list[Constituenta] = []
|
||||
self.by_id: dict[int, Constituenta] = {}
|
||||
self.by_alias: dict[str, Constituenta] = {}
|
||||
self.is_loaded = False
|
||||
self.is_loaded_terms = False
|
||||
|
||||
def ensure_loaded(self) -> None:
|
||||
if not self.is_loaded:
|
||||
self.constituents = list(
|
||||
self._schema.constituentsQ().only(
|
||||
'order',
|
||||
'alias',
|
||||
'cst_type',
|
||||
'definition_formal',
|
||||
'term_raw',
|
||||
'definition_raw'
|
||||
).order_by('order')
|
||||
)
|
||||
self.by_id = {cst.pk: cst for cst in self.constituents}
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
self.is_loaded = True
|
||||
self.is_loaded_terms = False
|
||||
|
||||
def ensure_loaded_terms(self) -> None:
|
||||
if not self.is_loaded_terms:
|
||||
self.constituents = list(
|
||||
self._schema.constituentsQ().only(
|
||||
'order',
|
||||
'alias',
|
||||
'cst_type',
|
||||
'definition_formal',
|
||||
'term_raw',
|
||||
'definition_raw',
|
||||
'term_forms',
|
||||
'term_resolved'
|
||||
).order_by('order')
|
||||
)
|
||||
self.by_id = {cst.pk: cst for cst in self.constituents}
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
self.is_loaded = True
|
||||
self.is_loaded_terms = True
|
||||
|
||||
def reload_aliases(self) -> None:
|
||||
self.by_alias = {cst.alias: cst for cst in self.constituents}
|
||||
|
||||
def clear(self) -> None:
|
||||
self.constituents = []
|
||||
self.by_id = {}
|
||||
self.by_alias = {}
|
||||
self.is_loaded = False
|
||||
self.is_loaded_terms = False
|
||||
|
||||
def insert(self, cst: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.insert(cst.order, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def insert_multi(self, items: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in items:
|
||||
self.constituents.insert(cst.order, cst)
|
||||
self.by_id[cst.pk] = cst
|
||||
self.by_alias[cst.alias] = cst
|
||||
|
||||
def remove(self, target: Constituenta) -> None:
|
||||
if self.is_loaded:
|
||||
self.constituents.remove(self.by_id[target.pk])
|
||||
del self.by_id[target.pk]
|
||||
del self.by_alias[target.alias]
|
||||
|
||||
def remove_multi(self, target: Iterable[Constituenta]) -> None:
|
||||
if self.is_loaded:
|
||||
for cst in target:
|
||||
self.constituents.remove(self.by_id[cst.pk])
|
||||
del self.by_id[cst.pk]
|
||||
del self.by_alias[cst.alias]
|
|
@ -1,136 +0,0 @@
|
|||
''' Models: RSForm semantic information. '''
|
||||
from typing import cast
|
||||
|
||||
from .api_RSLanguage import (
|
||||
infer_template,
|
||||
is_base_set,
|
||||
is_functional,
|
||||
is_simple_expression,
|
||||
split_template
|
||||
)
|
||||
from .Constituenta import Constituenta, CstType, extract_globals
|
||||
from .RSForm import RSForm
|
||||
from .RSFormCached import RSFormCached
|
||||
|
||||
|
||||
class SemanticInfo:
|
||||
''' Semantic information derived from constituents. '''
|
||||
|
||||
def __init__(self, schema: RSFormCached):
|
||||
schema.cache.ensure_loaded()
|
||||
self._items = schema.cache.constituents
|
||||
self._cst_by_ID = schema.cache.by_id
|
||||
self._cst_by_alias = schema.cache.by_alias
|
||||
self.graph = RSForm.graph_formal(schema.cache.constituents, schema.cache.by_alias)
|
||||
self.info = {
|
||||
cst.pk: {
|
||||
'is_simple': False,
|
||||
'is_template': False,
|
||||
'parent': cst.pk,
|
||||
'children': []
|
||||
}
|
||||
for cst in schema.cache.constituents
|
||||
}
|
||||
self._calculate_attributes()
|
||||
|
||||
def __getitem__(self, key: int) -> dict:
|
||||
return self.info[key]
|
||||
|
||||
def is_simple_expression(self, target: int) -> bool:
|
||||
''' Access "is_simple" attribute. '''
|
||||
return cast(bool, self.info[target]['is_simple'])
|
||||
|
||||
def is_template(self, target: int) -> bool:
|
||||
''' Access "is_template" attribute. '''
|
||||
return cast(bool, self.info[target]['is_template'])
|
||||
|
||||
def parent(self, target: int) -> int:
|
||||
''' Access "parent" attribute. '''
|
||||
return cast(int, self.info[target]['parent'])
|
||||
|
||||
def children(self, target: int) -> list[int]:
|
||||
''' Access "children" attribute. '''
|
||||
return cast(list[int], self.info[target]['children'])
|
||||
|
||||
def _calculate_attributes(self) -> None:
|
||||
for cst_id in self.graph.topological_order():
|
||||
cst = self._cst_by_ID[cst_id]
|
||||
self.info[cst_id]['is_template'] = infer_template(cst.definition_formal)
|
||||
self.info[cst_id]['is_simple'] = self._infer_simple_expression(cst)
|
||||
if not self.info[cst_id]['is_simple'] or cst.cst_type == CstType.STRUCTURED:
|
||||
continue
|
||||
parent = self._infer_parent(cst)
|
||||
self.info[cst_id]['parent'] = parent
|
||||
if parent != cst_id:
|
||||
cast(list[int], self.info[parent]['children']).append(cst_id)
|
||||
|
||||
def _infer_simple_expression(self, target: Constituenta) -> bool:
|
||||
if target.cst_type == CstType.STRUCTURED or is_base_set(target.cst_type):
|
||||
return False
|
||||
|
||||
dependencies = self.graph.inputs[target.pk]
|
||||
has_complex_dependency = any(
|
||||
self.is_template(cst_id) and
|
||||
not self.is_simple_expression(cst_id) for cst_id in dependencies
|
||||
)
|
||||
if has_complex_dependency:
|
||||
return False
|
||||
|
||||
if is_functional(target.cst_type):
|
||||
return is_simple_expression(split_template(target.definition_formal)['body'])
|
||||
else:
|
||||
return is_simple_expression(target.definition_formal)
|
||||
|
||||
def _infer_parent(self, target: Constituenta) -> int:
|
||||
sources = self._extract_sources(target)
|
||||
if len(sources) != 1:
|
||||
return target.pk
|
||||
|
||||
parent_id = next(iter(sources))
|
||||
parent = self._cst_by_ID[parent_id]
|
||||
if is_base_set(parent.cst_type):
|
||||
return target.pk
|
||||
return parent_id
|
||||
|
||||
def _extract_sources(self, target: Constituenta) -> set[int]:
|
||||
sources: set[int] = set()
|
||||
if not is_functional(target.cst_type):
|
||||
for parent_id in self.graph.inputs[target.pk]:
|
||||
parent_info = self[parent_id]
|
||||
if not parent_info['is_template'] or not parent_info['is_simple']:
|
||||
sources.add(parent_info['parent'])
|
||||
return sources
|
||||
|
||||
expression = split_template(target.definition_formal)
|
||||
body_dependencies = extract_globals(expression['body'])
|
||||
for alias in body_dependencies:
|
||||
parent = self._cst_by_alias.get(alias)
|
||||
if not parent:
|
||||
continue
|
||||
|
||||
parent_info = self[parent.pk]
|
||||
if not parent_info['is_template'] or not parent_info['is_simple']:
|
||||
sources.add(parent_info['parent'])
|
||||
|
||||
if self._need_check_head(sources, expression['head']):
|
||||
head_dependencies = extract_globals(expression['head'])
|
||||
for alias in head_dependencies:
|
||||
parent = self._cst_by_alias.get(alias)
|
||||
if not parent:
|
||||
continue
|
||||
|
||||
parent_info = self[parent.pk]
|
||||
if not is_base_set(parent.cst_type) and \
|
||||
(not parent_info['is_template'] or not parent_info['is_simple']):
|
||||
sources.add(parent_info['parent'])
|
||||
return sources
|
||||
|
||||
def _need_check_head(self, sources: set[int], head: str) -> bool:
|
||||
if len(sources) == 0:
|
||||
return True
|
||||
elif len(sources) != 1:
|
||||
return False
|
||||
else:
|
||||
base = self._cst_by_ID[next(iter(sources))]
|
||||
return not is_functional(base.cst_type) or \
|
||||
split_template(base.definition_formal)['head'] != head
|
|
@ -1,6 +1,4 @@
|
|||
''' Django: Models. '''
|
||||
|
||||
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
||||
from .OrderManager import OrderManager
|
||||
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
||||
from .RSFormCached import RSFormCached
|
||||
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm, SemanticInfo
|
||||
|
|
|
@ -12,7 +12,6 @@ from .basics import (
|
|||
WordFormSerializer
|
||||
)
|
||||
from .data_access import (
|
||||
CrucialUpdateSerializer,
|
||||
CstCreateSerializer,
|
||||
CstInfoSerializer,
|
||||
CstListSerializer,
|
||||
|
@ -25,6 +24,6 @@ from .data_access import (
|
|||
RSFormSerializer,
|
||||
SubstitutionSerializerBase
|
||||
)
|
||||
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer, generate_trs
|
||||
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
|
||||
from .io_pyconcept import PyConceptAdapter
|
||||
from .responses import NewCstResponse, NewMultiCstResponse, ResultTextResponse
|
||||
|
|
|
@ -46,16 +46,12 @@ class CstUpdateSerializer(StrictSerializer):
|
|||
class Meta:
|
||||
''' serializer metadata. '''
|
||||
model = Constituenta
|
||||
fields = 'alias', 'cst_type', 'convention', 'crucial', 'definition_formal', \
|
||||
'definition_raw', 'term_raw', 'term_forms'
|
||||
fields = 'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw', 'term_forms'
|
||||
|
||||
target = PKField(
|
||||
many=False,
|
||||
queryset=Constituenta.objects.all().only(
|
||||
'schema_id',
|
||||
'alias', 'cst_type', 'convention', 'crucial',
|
||||
'definition_formal', 'definition_raw', 'term_raw'
|
||||
)
|
||||
'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw')
|
||||
)
|
||||
item_data = ConstituentaUpdateData()
|
||||
|
||||
|
@ -68,31 +64,13 @@ class CstUpdateSerializer(StrictSerializer):
|
|||
})
|
||||
if 'alias' in attrs['item_data']:
|
||||
new_alias = attrs['item_data']['alias']
|
||||
if cst.alias != new_alias and Constituenta.objects.filter(schema=schema, alias=new_alias).exists():
|
||||
if cst.alias != new_alias and RSForm(schema).constituents().filter(alias=new_alias).exists():
|
||||
raise serializers.ValidationError({
|
||||
'alias': msg.aliasTaken(new_alias)
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class CrucialUpdateSerializer(StrictSerializer):
|
||||
''' Serializer: update crucial status. '''
|
||||
target = PKField(
|
||||
many=True,
|
||||
queryset=Constituenta.objects.all().only('crucial', 'schema_id')
|
||||
)
|
||||
value = serializers.BooleanField()
|
||||
|
||||
def validate(self, attrs):
|
||||
schema = cast(LibraryItem, self.context['schema'])
|
||||
for cst in attrs['target']:
|
||||
if schema and cst.schema_id != schema.pk:
|
||||
raise serializers.ValidationError({
|
||||
f'{cst.pk}': msg.constituentaNotInRSform(schema.title)
|
||||
})
|
||||
return attrs
|
||||
|
||||
|
||||
class CstDetailsSerializer(StrictModelSerializer):
|
||||
''' Serializer: Constituenta data including parse. '''
|
||||
parse = CstParseSerializer()
|
||||
|
@ -118,7 +96,7 @@ class CstCreateSerializer(StrictModelSerializer):
|
|||
''' serializer metadata. '''
|
||||
model = Constituenta
|
||||
fields = \
|
||||
'alias', 'cst_type', 'convention', 'crucial', \
|
||||
'alias', 'cst_type', 'convention', \
|
||||
'term_raw', 'definition_raw', 'definition_formal', \
|
||||
'insert_after', 'term_forms'
|
||||
|
||||
|
@ -164,7 +142,7 @@ class RSFormSerializer(StrictModelSerializer):
|
|||
result['items'] = []
|
||||
result['oss'] = []
|
||||
result['inheritance'] = []
|
||||
for cst in Constituenta.objects.filter(schema=instance).defer('order').order_by('order'):
|
||||
for cst in RSForm(instance).constituents().defer('order').order_by('order'):
|
||||
result['items'].append(CstInfoSerializer(cst).data)
|
||||
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
|
||||
result['oss'].append({
|
||||
|
@ -197,18 +175,17 @@ class RSFormSerializer(StrictModelSerializer):
|
|||
|
||||
def restore_from_version(self, data: dict):
|
||||
''' Load data from version. '''
|
||||
instance = cast(LibraryItem, self.instance)
|
||||
schema = RSForm(instance)
|
||||
schema = RSForm(cast(LibraryItem, self.instance))
|
||||
items: list[dict] = data['items']
|
||||
ids: list[int] = [item['id'] for item in items]
|
||||
processed: list[int] = []
|
||||
|
||||
for cst in schema.constituentsQ():
|
||||
for cst in schema.constituents():
|
||||
if not cst.pk in ids:
|
||||
cst.delete()
|
||||
else:
|
||||
cst_data = next(x for x in items if x['id'] == cst.pk)
|
||||
cst_data['schema'] = instance.pk
|
||||
cst_data['schema'] = cast(LibraryItem, self.instance).pk
|
||||
new_cst = CstBaseSerializer(data=cst_data)
|
||||
new_cst.is_valid(raise_exception=True)
|
||||
new_cst.validated_data['order'] = ids.index(cst.pk)
|
||||
|
@ -220,10 +197,10 @@ class RSFormSerializer(StrictModelSerializer):
|
|||
|
||||
for cst_data in items:
|
||||
if cst_data['id'] not in processed:
|
||||
cst = schema.insert_last(cst_data['alias'])
|
||||
cst = schema.insert_new(cst_data['alias'])
|
||||
old_id = cst_data['id']
|
||||
cst_data['id'] = cst.pk
|
||||
cst_data['schema'] = instance.pk
|
||||
cst_data['schema'] = cast(LibraryItem, self.instance).pk
|
||||
new_cst = CstBaseSerializer(data=cst_data)
|
||||
new_cst.is_valid(raise_exception=True)
|
||||
new_cst.validated_data['order'] = ids.index(old_id)
|
||||
|
|
|
@ -6,7 +6,7 @@ from apps.library.models import LibraryItem
|
|||
from shared import messages as msg
|
||||
from shared.serializers import StrictSerializer
|
||||
|
||||
from ..models import Constituenta, RSFormCached
|
||||
from ..models import Constituenta, RSForm
|
||||
from ..utils import fix_old_references
|
||||
|
||||
_CST_TYPE = 'constituenta'
|
||||
|
@ -27,12 +27,33 @@ class RSFormUploadSerializer(StrictSerializer):
|
|||
load_metadata = serializers.BooleanField()
|
||||
|
||||
|
||||
def generate_trs(schema: LibraryItem) -> dict:
|
||||
''' Generate TRS file for RSForm. '''
|
||||
items = []
|
||||
for cst in Constituenta.objects.filter(schema=schema).order_by('order'):
|
||||
items.append(
|
||||
{
|
||||
class RSFormTRSSerializer(serializers.Serializer):
|
||||
''' Serializer: TRS file production and loading for RSForm. '''
|
||||
|
||||
def to_representation(self, instance: RSForm) -> dict:
|
||||
result = self._prepare_json_rsform(instance.model)
|
||||
items = instance.constituents().order_by('order')
|
||||
for cst in items:
|
||||
result['items'].append(self._prepare_json_constituenta(cst))
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _prepare_json_rsform(schema: LibraryItem) -> dict:
|
||||
return {
|
||||
'type': _TRS_TYPE,
|
||||
'title': schema.title,
|
||||
'alias': schema.alias,
|
||||
'comment': schema.description,
|
||||
'items': [],
|
||||
'claimed': False,
|
||||
'selection': [],
|
||||
'version': _TRS_VERSION,
|
||||
'versionInfo': _TRS_HEADER
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _prepare_json_constituenta(cst: Constituenta) -> dict:
|
||||
return {
|
||||
'entityUID': cst.pk,
|
||||
'type': _CST_TYPE,
|
||||
'cstType': cst.cst_type,
|
||||
|
@ -51,25 +72,8 @@ def generate_trs(schema: LibraryItem) -> dict:
|
|||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
return {
|
||||
'type': _TRS_TYPE,
|
||||
'title': schema.title,
|
||||
'alias': schema.alias,
|
||||
'comment': schema.description,
|
||||
'items': items,
|
||||
'claimed': False,
|
||||
'selection': [],
|
||||
'version': _TRS_VERSION,
|
||||
'versionInfo': _TRS_HEADER
|
||||
}
|
||||
|
||||
|
||||
class RSFormTRSSerializer(serializers.Serializer):
|
||||
''' Serializer: TRS file production and loading for RSForm. '''
|
||||
|
||||
@staticmethod
|
||||
def load_versioned_data(data: dict) -> dict:
|
||||
def from_versioned_data(self, data: dict) -> dict:
|
||||
''' Load data from version. '''
|
||||
result = {
|
||||
'type': _TRS_TYPE,
|
||||
|
@ -123,7 +127,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
result['description'] = data.get('description', '')
|
||||
if 'id' in data:
|
||||
result['id'] = data['id']
|
||||
self.instance = RSFormCached.from_id(result['id'])
|
||||
self.instance = RSForm.from_id(result['id'])
|
||||
return result
|
||||
|
||||
def validate(self, attrs: dict):
|
||||
|
@ -136,8 +140,8 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
return attrs
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data: dict) -> RSFormCached:
|
||||
self.instance: RSFormCached = RSFormCached.create(
|
||||
def create(self, validated_data: dict) -> RSForm:
|
||||
self.instance: RSForm = RSForm.create(
|
||||
owner=validated_data.get('owner', None),
|
||||
alias=validated_data['alias'],
|
||||
title=validated_data['title'],
|
||||
|
@ -147,6 +151,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
access_policy=validated_data['access_policy'],
|
||||
location=validated_data['location']
|
||||
)
|
||||
self.instance.save()
|
||||
order = 0
|
||||
for cst_data in validated_data['items']:
|
||||
cst = Constituenta(
|
||||
|
@ -162,7 +167,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
return self.instance
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance: RSFormCached, validated_data) -> RSFormCached:
|
||||
def update(self, instance: RSForm, validated_data) -> RSForm:
|
||||
if 'alias' in validated_data:
|
||||
instance.model.alias = validated_data['alias']
|
||||
if 'title' in validated_data:
|
||||
|
@ -171,7 +176,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
instance.model.description = validated_data['description']
|
||||
|
||||
order = 0
|
||||
prev_constituents = instance.constituentsQ()
|
||||
prev_constituents = instance.constituents()
|
||||
loaded_ids = set()
|
||||
for cst_data in validated_data['items']:
|
||||
uid = int(cst_data['entityUID'])
|
||||
|
@ -199,7 +204,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
|||
prev_cst.delete()
|
||||
|
||||
instance.resolve_all_text()
|
||||
instance.model.save()
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -6,20 +6,20 @@ import pyconcept
|
|||
|
||||
from shared import messages as msg
|
||||
|
||||
from ..models import Constituenta
|
||||
from ..models import RSForm
|
||||
|
||||
|
||||
class PyConceptAdapter:
|
||||
''' RSForm adapter for interacting with pyconcept module. '''
|
||||
|
||||
def __init__(self, data: Union[int, dict]):
|
||||
def __init__(self, data: Union[RSForm, dict]):
|
||||
try:
|
||||
if 'items' in cast(dict, data):
|
||||
self.data = self._prepare_request_raw(cast(dict, data))
|
||||
else:
|
||||
self.data = self._prepare_request(cast(int, data))
|
||||
self.data = self._prepare_request(cast(RSForm, data))
|
||||
except TypeError:
|
||||
self.data = self._prepare_request(cast(int, data))
|
||||
self.data = self._prepare_request(cast(RSForm, data))
|
||||
self._checked_data: Optional[dict] = None
|
||||
|
||||
def parse(self) -> dict:
|
||||
|
@ -30,11 +30,11 @@ class PyConceptAdapter:
|
|||
raise ValueError(msg.pyconceptFailure())
|
||||
return self._checked_data
|
||||
|
||||
def _prepare_request(self, schemaID: int) -> dict:
|
||||
def _prepare_request(self, schema: RSForm) -> dict:
|
||||
result: dict = {
|
||||
'items': []
|
||||
}
|
||||
items = Constituenta.objects.filter(schema_id=schemaID).order_by('order')
|
||||
items = schema.constituents().order_by('order')
|
||||
for cst in items:
|
||||
result['items'].append({
|
||||
'entityUID': cst.pk,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
''' Tests for Django Models. '''
|
||||
from .t_Constituenta import *
|
||||
from .t_RSForm import *
|
||||
from .t_RSFormCached import *
|
||||
|
|
|
@ -14,46 +14,333 @@ class TestRSForm(DBTester):
|
|||
super().setUp()
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.assertNotEqual(self.user1, self.user2)
|
||||
self.schema = RSForm.create(title='Test')
|
||||
self.assertNotEqual(self.user1, self.user2)
|
||||
|
||||
|
||||
def test_constituents(self):
|
||||
schema1 = RSForm.create(title='Test1')
|
||||
schema2 = RSForm.create(title='Test2')
|
||||
self.assertFalse(schema1.constituentsQ().exists())
|
||||
self.assertFalse(schema2.constituentsQ().exists())
|
||||
self.assertFalse(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
|
||||
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0)
|
||||
Constituenta.objects.create(alias='X2', schema=schema1.model, order=1)
|
||||
self.assertTrue(schema1.constituentsQ().exists())
|
||||
self.assertFalse(schema2.constituentsQ().exists())
|
||||
self.assertEqual(schema1.constituentsQ().count(), 2)
|
||||
self.assertTrue(schema1.constituents().exists())
|
||||
self.assertFalse(schema2.constituents().exists())
|
||||
self.assertEqual(schema1.constituents().count(), 2)
|
||||
|
||||
|
||||
def test_get_max_index(self):
|
||||
schema1 = RSForm.create(title='Test1')
|
||||
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0)
|
||||
Constituenta.objects.create(alias='D2', cst_type=CstType.TERM, schema=schema1.model, order=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.AXIOM), 0)
|
||||
|
||||
|
||||
def test_insert_at(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
x1 = schema.insert_new('X1')
|
||||
self.assertEqual(x1.order, 0)
|
||||
self.assertEqual(x1.schema, schema.model)
|
||||
|
||||
x2 = schema.insert_new('X2', position=0)
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(x2.schema, schema.model)
|
||||
self.assertEqual(x1.order, 1)
|
||||
|
||||
x3 = schema.insert_new('X3', position=3)
|
||||
x2.refresh_from_db()
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x3.order, 2)
|
||||
self.assertEqual(x3.schema, schema.model)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(x1.order, 1)
|
||||
|
||||
x4 = schema.insert_new('X4', position=2)
|
||||
x3.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x4.order, 2)
|
||||
self.assertEqual(x4.schema, schema.model)
|
||||
self.assertEqual(x3.order, 3)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(x1.order, 1)
|
||||
|
||||
|
||||
def test_insert_at_invalid_position(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.schema.insert_new('X5', position=-2)
|
||||
|
||||
|
||||
def test_insert_at_invalid_alias(self):
|
||||
self.schema.insert_last('X1')
|
||||
self.schema.insert_new('X1')
|
||||
with self.assertRaises(ValidationError):
|
||||
self.schema.insert_last('X1')
|
||||
self.schema.insert_new('X1')
|
||||
|
||||
|
||||
def test_insert_at_reorder(self):
|
||||
self.schema.insert_new('X1')
|
||||
d1 = self.schema.insert_new('D1')
|
||||
d2 = self.schema.insert_new('D2', position=0)
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(d1.order, 2)
|
||||
self.assertEqual(d2.order, 0)
|
||||
|
||||
x2 = self.schema.insert_new('X2', position=3)
|
||||
self.assertEqual(x2.order, 3)
|
||||
|
||||
|
||||
def test_insert_last(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x1 = self.schema.insert_new('X1')
|
||||
self.assertEqual(x1.order, 0)
|
||||
self.assertEqual(x1.schema, self.schema.model)
|
||||
|
||||
x2 = self.schema.insert_last('X2')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(x2.schema, self.schema.model)
|
||||
self.assertEqual(x1.order, 0)
|
||||
|
||||
|
||||
def test_create_cst(self):
|
||||
data = {
|
||||
'alias': 'X3',
|
||||
'cst_type': CstType.BASE,
|
||||
'term_raw': 'слон',
|
||||
'definition_raw': 'test',
|
||||
'convention': 'convention'
|
||||
}
|
||||
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
x3 = self.schema.create_cst(data=data, insert_after=x1)
|
||||
x2.refresh_from_db()
|
||||
|
||||
self.assertEqual(x3.alias, data['alias'])
|
||||
self.assertEqual(x3.term_raw, data['term_raw'])
|
||||
self.assertEqual(x3.definition_raw, data['definition_raw'])
|
||||
self.assertEqual(x2.order, 2)
|
||||
self.assertEqual(x3.order, 1)
|
||||
|
||||
|
||||
def test_create_cst_resolve(self):
|
||||
x1 = self.schema.insert_new(
|
||||
alias='X1',
|
||||
term_raw='@{X2|datv}',
|
||||
definition_raw='@{X1|datv} @{X2|datv}'
|
||||
)
|
||||
x2 = self.schema.create_cst({
|
||||
'alias': 'X2',
|
||||
'cst_type': CstType.BASE,
|
||||
'term_raw': 'слон',
|
||||
'definition_raw': '@{X1|plur} @{X2|plur}'
|
||||
})
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x1.term_resolved, 'слону')
|
||||
self.assertEqual(x1.definition_resolved, 'слону слону')
|
||||
self.assertEqual(x2.term_resolved, 'слон')
|
||||
self.assertEqual(x2.definition_resolved, 'слонам слоны')
|
||||
|
||||
|
||||
def test_insert_copy(self):
|
||||
x1 = self.schema.insert_new(
|
||||
alias='X10',
|
||||
convention='Test'
|
||||
)
|
||||
s1 = self.schema.insert_new(
|
||||
alias='S11',
|
||||
definition_formal=x1.alias,
|
||||
definition_raw='@{X10|plur}'
|
||||
)
|
||||
|
||||
result = self.schema.insert_copy([s1, x1], 1)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
s1.refresh_from_db()
|
||||
self.assertEqual(s1.order, 3)
|
||||
|
||||
x2 = result[1]
|
||||
self.assertEqual(x2.order, 2)
|
||||
self.assertEqual(x2.alias, 'X11')
|
||||
self.assertEqual(x2.cst_type, CstType.BASE)
|
||||
self.assertEqual(x2.convention, x1.convention)
|
||||
|
||||
s2 = result[0]
|
||||
self.assertEqual(s2.order, 1)
|
||||
self.assertEqual(s2.alias, 'S12')
|
||||
self.assertEqual(s2.cst_type, CstType.STRUCTURED)
|
||||
self.assertEqual(s2.definition_formal, x2.alias)
|
||||
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
||||
|
||||
|
||||
def test_delete_cst(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D1',
|
||||
definition_formal='X1 = X2',
|
||||
definition_raw='@{X1|sing}',
|
||||
term_raw='@{X2|plur}'
|
||||
)
|
||||
|
||||
self.schema.delete_cst([x1])
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituents().count(), 2)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(d1.order, 1)
|
||||
self.assertEqual(d1.definition_formal, 'DEL = X2')
|
||||
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
|
||||
self.assertEqual(d1.term_raw, '@{X2|plur}')
|
||||
|
||||
|
||||
def test_apply_mapping(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X11')
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D1',
|
||||
definition_formal='X1 = X11 = X2',
|
||||
definition_raw='@{X11|sing}',
|
||||
term_raw='@{X1|plur}'
|
||||
)
|
||||
|
||||
self.schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'})
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression')
|
||||
self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition')
|
||||
self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term')
|
||||
self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping')
|
||||
self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping')
|
||||
|
||||
|
||||
def test_substitute(self):
|
||||
x1 = self.schema.insert_new(
|
||||
alias='X1',
|
||||
term_raw='Test'
|
||||
)
|
||||
x2 = self.schema.insert_new(
|
||||
alias='X2',
|
||||
term_raw='Test2'
|
||||
)
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D1',
|
||||
definition_formal=x1.alias
|
||||
)
|
||||
|
||||
self.schema.substitute([(x1, x2)])
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituents().count(), 2)
|
||||
self.assertEqual(x2.term_raw, 'Test2')
|
||||
self.assertEqual(d1.definition_formal, x2.alias)
|
||||
|
||||
|
||||
def test_move_cst(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
d1 = self.schema.insert_new('D1')
|
||||
d2 = self.schema.insert_new('D2')
|
||||
self.schema.move_cst([x2, d2], 0)
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
self.assertEqual(x1.order, 2)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(d1.order, 3)
|
||||
self.assertEqual(d2.order, 1)
|
||||
|
||||
|
||||
def test_move_cst_down(self):
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
self.schema.move_cst([x1], 1)
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
self.assertEqual(x1.order, 1)
|
||||
self.assertEqual(x2.order, 0)
|
||||
|
||||
|
||||
def test_restore_order(self):
|
||||
d2 = self.schema.insert_new(
|
||||
alias='D2',
|
||||
definition_formal=r'D{ξ∈S1 | 1=1}',
|
||||
)
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D1',
|
||||
definition_formal=r'Pr1(S1)\X1',
|
||||
)
|
||||
x1 = self.schema.insert_new('X1')
|
||||
x2 = self.schema.insert_new('X2')
|
||||
s1 = self.schema.insert_new(
|
||||
alias='S1',
|
||||
definition_formal='ℬ(X1×X1)'
|
||||
)
|
||||
c1 = self.schema.insert_new('C1')
|
||||
s2 = self.schema.insert_new(
|
||||
alias='S2',
|
||||
definition_formal='ℬ(X2×D1)'
|
||||
)
|
||||
a1 = self.schema.insert_new(
|
||||
alias='A1',
|
||||
definition_formal=r'D3=∅',
|
||||
)
|
||||
d3 = self.schema.insert_new(
|
||||
alias='D3',
|
||||
definition_formal=r'Pr2(S2)',
|
||||
)
|
||||
f1 = self.schema.insert_new(
|
||||
alias='F1',
|
||||
definition_formal=r'[α∈ℬ(X1)] D{σ∈S1 | α⊆pr1(σ)}',
|
||||
)
|
||||
d4 = self.schema.insert_new(
|
||||
alias='D4',
|
||||
definition_formal=r'Pr2(D3)',
|
||||
)
|
||||
f2 = self.schema.insert_new(
|
||||
alias='F2',
|
||||
definition_formal=r'[α∈ℬ(X1)] X1\α',
|
||||
)
|
||||
|
||||
self.schema.restore_order()
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
c1.refresh_from_db()
|
||||
s1.refresh_from_db()
|
||||
s2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
d3.refresh_from_db()
|
||||
d4.refresh_from_db()
|
||||
f1.refresh_from_db()
|
||||
f2.refresh_from_db()
|
||||
a1.refresh_from_db()
|
||||
|
||||
self.assertEqual(x1.order, 0)
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(c1.order, 2)
|
||||
self.assertEqual(s1.order, 3)
|
||||
self.assertEqual(d1.order, 4)
|
||||
self.assertEqual(s2.order, 5)
|
||||
self.assertEqual(d3.order, 6)
|
||||
self.assertEqual(a1.order, 7)
|
||||
self.assertEqual(d4.order, 8)
|
||||
self.assertEqual(d2.order, 9)
|
||||
self.assertEqual(f1.order, 10)
|
||||
self.assertEqual(f2.order, 11)
|
||||
|
||||
|
||||
def test_reset_aliases(self):
|
||||
x1 = self.schema.insert_last(
|
||||
x1 = self.schema.insert_new(
|
||||
alias='X11',
|
||||
term_raw='человек',
|
||||
term_resolved='человек'
|
||||
)
|
||||
x2 = self.schema.insert_last('X21')
|
||||
d1 = self.schema.insert_last(
|
||||
x2 = self.schema.insert_new('X21')
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D11',
|
||||
definition_formal='X21=X21',
|
||||
term_raw='@{X21|sing}',
|
||||
|
@ -73,47 +360,46 @@ class TestRSForm(DBTester):
|
|||
self.assertEqual(d1.definition_raw, '@{X1|datv}')
|
||||
self.assertEqual(d1.definition_resolved, 'test')
|
||||
|
||||
def test_move_cst(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
d1 = self.schema.insert_last('D1')
|
||||
d2 = self.schema.insert_last('D2')
|
||||
self.schema.move_cst([x2, d2], 0)
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
self.assertEqual(x1.order, 2)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(d1.order, 3)
|
||||
self.assertEqual(d2.order, 1)
|
||||
|
||||
|
||||
def test_move_cst_down(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
self.schema.move_cst([x1], 1)
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
self.assertEqual(x1.order, 1)
|
||||
self.assertEqual(x2.order, 0)
|
||||
|
||||
def test_delete_cst(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
d1 = self.schema.insert_last(
|
||||
def test_on_term_change(self):
|
||||
x1 = self.schema.insert_new(
|
||||
alias='X1',
|
||||
term_raw='человек',
|
||||
term_resolved='человек',
|
||||
definition_raw='одному @{X1|datv}',
|
||||
definition_resolved='одному человеку',
|
||||
)
|
||||
x2 = self.schema.insert_new(
|
||||
alias='X2',
|
||||
term_raw='сильный @{X1|sing}',
|
||||
term_resolved='сильный человек',
|
||||
definition_raw=x1.definition_raw,
|
||||
definition_resolved=x1.definition_resolved
|
||||
)
|
||||
x3 = self.schema.insert_new(
|
||||
alias='X3',
|
||||
definition_raw=x1.definition_raw,
|
||||
definition_resolved=x1.definition_resolved
|
||||
)
|
||||
d1 = self.schema.insert_new(
|
||||
alias='D1',
|
||||
definition_formal='X1 = X2',
|
||||
definition_raw='@{X1|sing}',
|
||||
term_raw='@{X2|plur}'
|
||||
definition_raw='очень @{X2|sing}',
|
||||
definition_resolved='очень сильный человек'
|
||||
)
|
||||
|
||||
self.schema.delete_cst([x1])
|
||||
x1.term_raw = 'слон'
|
||||
x1.term_resolved = 'слон'
|
||||
x1.save()
|
||||
|
||||
self.schema.after_term_change([x1.pk])
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
x3.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituentsQ().count(), 2)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(d1.order, 1)
|
||||
self.assertEqual(d1.definition_formal, 'DEL = X2')
|
||||
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
|
||||
self.assertEqual(d1.term_raw, '@{X2|plur}')
|
||||
|
||||
self.assertEqual(x1.term_raw, 'слон')
|
||||
self.assertEqual(x1.term_resolved, 'слон')
|
||||
self.assertEqual(x1.definition_resolved, 'одному слону')
|
||||
self.assertEqual(x2.definition_resolved, x1.definition_resolved)
|
||||
self.assertEqual(x3.definition_resolved, x1.definition_resolved)
|
||||
self.assertEqual(d1.definition_resolved, 'очень сильный слон')
|
||||
|
|
|
@ -1,267 +0,0 @@
|
|||
''' Testing models: api_RSForm. '''
|
||||
from django.forms import ValidationError
|
||||
|
||||
from apps.rsform.models import Constituenta, CstType, OrderManager, RSFormCached
|
||||
from apps.users.models import User
|
||||
from shared.DBTester import DBTester
|
||||
|
||||
|
||||
class TestRSFormCached(DBTester):
|
||||
''' Testing RSForm Cached wrapper. '''
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user1 = User.objects.create(username='User1')
|
||||
self.user2 = User.objects.create(username='User2')
|
||||
self.assertNotEqual(self.user1, self.user2)
|
||||
self.schema = RSFormCached.create(title='Test')
|
||||
|
||||
|
||||
def test_constituents(self):
|
||||
schema1 = RSFormCached.create(title='Test1')
|
||||
schema2 = RSFormCached.create(title='Test2')
|
||||
self.assertFalse(schema1.constituentsQ().exists())
|
||||
self.assertFalse(schema2.constituentsQ().exists())
|
||||
|
||||
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0)
|
||||
Constituenta.objects.create(alias='X2', schema=schema1.model, order=1)
|
||||
self.assertTrue(schema1.constituentsQ().exists())
|
||||
self.assertFalse(schema2.constituentsQ().exists())
|
||||
self.assertEqual(schema1.constituentsQ().count(), 2)
|
||||
|
||||
|
||||
def test_insert_last(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
self.assertEqual(x1.order, 0)
|
||||
self.assertEqual(x1.schema, self.schema.model)
|
||||
|
||||
|
||||
def test_create_cst(self):
|
||||
data = {
|
||||
'alias': 'X3',
|
||||
'cst_type': CstType.BASE,
|
||||
'term_raw': 'слон',
|
||||
'definition_raw': 'test',
|
||||
'convention': 'convention'
|
||||
}
|
||||
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
x3 = self.schema.create_cst(data=data, insert_after=x1)
|
||||
x2.refresh_from_db()
|
||||
|
||||
self.assertEqual(x3.alias, data['alias'])
|
||||
self.assertEqual(x3.term_raw, data['term_raw'])
|
||||
self.assertEqual(x3.definition_raw, data['definition_raw'])
|
||||
self.assertEqual(x2.order, 2)
|
||||
self.assertEqual(x3.order, 1)
|
||||
|
||||
|
||||
def test_create_cst_resolve(self):
|
||||
x1 = self.schema.insert_last(
|
||||
alias='X1',
|
||||
term_raw='@{X2|datv}',
|
||||
definition_raw='@{X1|datv} @{X2|datv}'
|
||||
)
|
||||
x2 = self.schema.create_cst({
|
||||
'alias': 'X2',
|
||||
'cst_type': CstType.BASE,
|
||||
'term_raw': 'слон',
|
||||
'definition_raw': '@{X1|plur} @{X2|plur}'
|
||||
})
|
||||
x1.refresh_from_db()
|
||||
self.assertEqual(x1.term_resolved, 'слону')
|
||||
self.assertEqual(x1.definition_resolved, 'слону слону')
|
||||
self.assertEqual(x2.term_resolved, 'слон')
|
||||
self.assertEqual(x2.definition_resolved, 'слонам слоны')
|
||||
|
||||
|
||||
def test_insert_copy(self):
|
||||
x1 = self.schema.insert_last(
|
||||
alias='X10',
|
||||
convention='Test'
|
||||
)
|
||||
s1 = self.schema.insert_last(
|
||||
alias='S11',
|
||||
definition_formal=x1.alias,
|
||||
definition_raw='@{X10|plur}'
|
||||
)
|
||||
|
||||
result = self.schema.insert_copy([s1, x1], 1)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
s1.refresh_from_db()
|
||||
self.assertEqual(s1.order, 3)
|
||||
|
||||
x2 = result[1]
|
||||
self.assertEqual(x2.order, 2)
|
||||
self.assertEqual(x2.alias, 'X11')
|
||||
self.assertEqual(x2.cst_type, CstType.BASE)
|
||||
self.assertEqual(x2.convention, x1.convention)
|
||||
|
||||
s2 = result[0]
|
||||
self.assertEqual(s2.order, 1)
|
||||
self.assertEqual(s2.alias, 'S12')
|
||||
self.assertEqual(s2.cst_type, CstType.STRUCTURED)
|
||||
self.assertEqual(s2.definition_formal, x2.alias)
|
||||
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
||||
|
||||
|
||||
def test_delete_cst(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
d1 = self.schema.insert_last(
|
||||
alias='D1',
|
||||
definition_formal='X1 = X2',
|
||||
definition_raw='@{X1|sing}',
|
||||
term_raw='@{X2|plur}'
|
||||
)
|
||||
|
||||
self.schema.delete_cst([x1.pk])
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituentsQ().count(), 2)
|
||||
self.assertEqual(x2.order, 0)
|
||||
self.assertEqual(d1.order, 1)
|
||||
self.assertEqual(d1.definition_formal, 'DEL = X2')
|
||||
self.assertEqual(d1.definition_raw, '@{DEL|sing}')
|
||||
self.assertEqual(d1.term_raw, '@{X2|plur}')
|
||||
|
||||
|
||||
def test_apply_mapping(self):
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X11')
|
||||
d1 = self.schema.insert_last(
|
||||
alias='D1',
|
||||
definition_formal='X1 = X11 = X2',
|
||||
definition_raw='@{X11|sing}',
|
||||
term_raw='@{X1|plur}'
|
||||
)
|
||||
|
||||
self.schema.apply_mapping({x1.alias: 'X3', x2.alias: 'X4'})
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(d1.definition_formal, 'X3 = X4 = X2', msg='Map IDs in expression')
|
||||
self.assertEqual(d1.definition_raw, '@{X4|sing}', msg='Map IDs in definition')
|
||||
self.assertEqual(d1.term_raw, '@{X3|plur}', msg='Map IDs in term')
|
||||
self.assertEqual(d1.term_resolved, '', msg='Do not run resolve on mapping')
|
||||
self.assertEqual(d1.definition_resolved, '', msg='Do not run resolve on mapping')
|
||||
|
||||
|
||||
def test_substitute(self):
|
||||
x1 = self.schema.insert_last(
|
||||
alias='X1',
|
||||
term_raw='Test'
|
||||
)
|
||||
x2 = self.schema.insert_last(
|
||||
alias='X2',
|
||||
term_raw='Test2'
|
||||
)
|
||||
d1 = self.schema.insert_last(
|
||||
alias='D1',
|
||||
definition_formal=x1.alias
|
||||
)
|
||||
|
||||
self.schema.substitute([(x1, x2)])
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
self.assertEqual(self.schema.constituentsQ().count(), 2)
|
||||
self.assertEqual(x2.term_raw, 'Test2')
|
||||
self.assertEqual(d1.definition_formal, x2.alias)
|
||||
|
||||
|
||||
def test_restore_order(self):
|
||||
d2 = self.schema.insert_last(
|
||||
alias='D2',
|
||||
definition_formal=r'D{ξ∈S1 | 1=1}',
|
||||
)
|
||||
d1 = self.schema.insert_last(
|
||||
alias='D1',
|
||||
definition_formal=r'Pr1(S1)\X1',
|
||||
)
|
||||
x1 = self.schema.insert_last('X1')
|
||||
x2 = self.schema.insert_last('X2')
|
||||
s1 = self.schema.insert_last(
|
||||
alias='S1',
|
||||
definition_formal='ℬ(X1×X1)'
|
||||
)
|
||||
c1 = self.schema.insert_last('C1')
|
||||
s2 = self.schema.insert_last(
|
||||
alias='S2',
|
||||
definition_formal='ℬ(X2×D1)'
|
||||
)
|
||||
a1 = self.schema.insert_last(
|
||||
alias='A1',
|
||||
definition_formal=r'D3=∅',
|
||||
)
|
||||
d3 = self.schema.insert_last(
|
||||
alias='D3',
|
||||
definition_formal=r'Pr2(S2)',
|
||||
)
|
||||
f1 = self.schema.insert_last(
|
||||
alias='F1',
|
||||
definition_formal=r'[α∈ℬ(X1)] D{σ∈S1 | α⊆pr1(σ)}',
|
||||
)
|
||||
d4 = self.schema.insert_last(
|
||||
alias='D4',
|
||||
definition_formal=r'Pr2(D3)',
|
||||
)
|
||||
f2 = self.schema.insert_last(
|
||||
alias='F2',
|
||||
definition_formal=r'[α∈ℬ(X1)] X1\α',
|
||||
)
|
||||
|
||||
OrderManager(self.schema).restore_order()
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
c1.refresh_from_db()
|
||||
s1.refresh_from_db()
|
||||
s2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
d2.refresh_from_db()
|
||||
d3.refresh_from_db()
|
||||
d4.refresh_from_db()
|
||||
f1.refresh_from_db()
|
||||
f2.refresh_from_db()
|
||||
a1.refresh_from_db()
|
||||
|
||||
self.assertEqual(x1.order, 0)
|
||||
self.assertEqual(x2.order, 1)
|
||||
self.assertEqual(c1.order, 2)
|
||||
self.assertEqual(s1.order, 3)
|
||||
self.assertEqual(d1.order, 4)
|
||||
self.assertEqual(s2.order, 5)
|
||||
self.assertEqual(d3.order, 6)
|
||||
self.assertEqual(a1.order, 7)
|
||||
self.assertEqual(d4.order, 8)
|
||||
self.assertEqual(d2.order, 9)
|
||||
self.assertEqual(f1.order, 10)
|
||||
self.assertEqual(f2.order, 11)
|
||||
|
||||
|
||||
def test_reset_aliases(self):
|
||||
x1 = self.schema.insert_last(
|
||||
alias='X11',
|
||||
term_raw='человек',
|
||||
term_resolved='человек'
|
||||
)
|
||||
x2 = self.schema.insert_last('X21')
|
||||
d1 = self.schema.insert_last(
|
||||
alias='D11',
|
||||
definition_formal='X21=X21',
|
||||
term_raw='@{X21|sing}',
|
||||
definition_raw='@{X11|datv}',
|
||||
definition_resolved='test'
|
||||
)
|
||||
|
||||
self.schema.reset_aliases()
|
||||
x1.refresh_from_db()
|
||||
x2.refresh_from_db()
|
||||
d1.refresh_from_db()
|
||||
|
||||
self.assertEqual(x1.alias, 'X1')
|
||||
self.assertEqual(x2.alias, 'X2')
|
||||
self.assertEqual(d1.alias, 'D1')
|
||||
self.assertEqual(d1.term_raw, '@{X2|sing}')
|
||||
self.assertEqual(d1.definition_raw, '@{X1|datv}')
|
||||
self.assertEqual(d1.definition_resolved, 'test')
|
|
@ -73,12 +73,12 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/details', method='get')
|
||||
def test_details(self):
|
||||
x1 = self.owned.insert_last(
|
||||
x1 = self.owned.insert_new(
|
||||
alias='X1',
|
||||
term_raw='человек',
|
||||
term_resolved='человек'
|
||||
)
|
||||
x2 = self.owned.insert_last(
|
||||
x2 = self.owned.insert_new(
|
||||
alias='X2',
|
||||
term_raw='@{X1|plur}',
|
||||
term_resolved='люди'
|
||||
|
@ -115,7 +115,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/check-expression', method='post')
|
||||
def test_check_expression(self):
|
||||
self.owned.insert_last('X1')
|
||||
self.owned.insert_new('X1')
|
||||
data = {'expression': 'X1=X1'}
|
||||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['parseResult'], True)
|
||||
|
@ -129,7 +129,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
|
||||
def test_check_constituenta(self):
|
||||
self.owned.insert_last('X1')
|
||||
self.owned.insert_new('X1')
|
||||
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
|
||||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['parseResult'], True)
|
||||
|
@ -141,7 +141,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
|
||||
def test_check_constituenta_error(self):
|
||||
self.owned.insert_last('X1')
|
||||
self.owned.insert_new('X1')
|
||||
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
|
||||
response = self.executeOK(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['parseResult'], False)
|
||||
|
@ -149,7 +149,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
|
||||
@decl_endpoint('/api/rsforms/{item}/resolve', method='post')
|
||||
def test_resolve(self):
|
||||
x1 = self.owned.insert_last(
|
||||
x1 = self.owned.insert_new(
|
||||
alias='X1',
|
||||
term_resolved='синий слон'
|
||||
)
|
||||
|
@ -191,7 +191,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
@decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
|
||||
def test_export_trs(self):
|
||||
schema = RSForm.create(title='Test')
|
||||
schema.insert_last('X1')
|
||||
schema.insert_new('X1')
|
||||
response = self.executeOK(item=schema.model.pk)
|
||||
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
|
||||
with io.BytesIO(response.content) as stream:
|
||||
|
@ -206,8 +206,8 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.executeForbidden(data=data, item=self.unowned_id)
|
||||
|
||||
data = {'alias': 'X3'}
|
||||
self.owned.insert_last('X1')
|
||||
x2 = self.owned.insert_last('X2')
|
||||
self.owned.insert_new('X1')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
self.executeBadData(item=self.owned_id)
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
|
@ -225,9 +225,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
'cst_type': CstType.BASE,
|
||||
'insert_after': x2.pk,
|
||||
'term_raw': 'test',
|
||||
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
|
||||
'definition_formal': 'invalid',
|
||||
'crucial': True
|
||||
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
|
||||
}
|
||||
response = self.executeCreated(data=data, item=self.owned_id)
|
||||
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
|
||||
|
@ -235,8 +233,6 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.assertEqual(x4.order, 2)
|
||||
self.assertEqual(x4.term_raw, data['term_raw'])
|
||||
self.assertEqual(x4.term_forms, data['term_forms'])
|
||||
self.assertEqual(x4.definition_formal, data['definition_formal'])
|
||||
self.assertEqual(x4.crucial, data['crucial'])
|
||||
|
||||
data = {
|
||||
'alias': 'X5',
|
||||
|
@ -251,11 +247,11 @@ class TestRSFormViewset(EndpointTester):
|
|||
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
|
||||
def test_substitute_multiple(self):
|
||||
self.set_params(item=self.owned_id)
|
||||
x1 = self.owned.insert_last('X1')
|
||||
x2 = self.owned.insert_last('X2')
|
||||
d1 = self.owned.insert_last('D1')
|
||||
d2 = self.owned.insert_last('D2')
|
||||
d3 = self.owned.insert_last(
|
||||
x1 = self.owned.insert_new('X1')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
d1 = self.owned.insert_new('D1')
|
||||
d2 = self.owned.insert_new('D2')
|
||||
d3 = self.owned.insert_new(
|
||||
alias='D3',
|
||||
definition_formal=r'X1 \ X2'
|
||||
)
|
||||
|
@ -318,19 +314,19 @@ class TestRSFormViewset(EndpointTester):
|
|||
data = {'items': [1337]}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
x1 = self.owned.insert_last('X1')
|
||||
x2 = self.owned.insert_last('X2')
|
||||
x1 = self.owned.insert_new('X1')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
|
||||
data = {'items': [x1.pk]}
|
||||
response = self.executeOK(data=data)
|
||||
x2.refresh_from_db()
|
||||
self.owned.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(len(response.data['items']), 1)
|
||||
self.assertEqual(self.owned.constituentsQ().count(), 1)
|
||||
self.assertEqual(self.owned.constituents().count(), 1)
|
||||
self.assertEqual(x2.alias, 'X2')
|
||||
self.assertEqual(x2.order, 0)
|
||||
|
||||
x3 = self.unowned.insert_last('X1')
|
||||
x3 = self.unowned.insert_new('X1')
|
||||
data = {'items': [x3.pk]}
|
||||
self.executeBadData(data=data, item=self.owned_id)
|
||||
|
||||
|
@ -342,8 +338,8 @@ class TestRSFormViewset(EndpointTester):
|
|||
data = {'items': [1337], 'move_to': 0}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
x1 = self.owned.insert_last('X1')
|
||||
x2 = self.owned.insert_last('X2')
|
||||
x1 = self.owned.insert_new('X1')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
|
||||
data = {'items': [x2.pk], 'move_to': 0}
|
||||
response = self.executeOK(data=data)
|
||||
|
@ -353,7 +349,7 @@ class TestRSFormViewset(EndpointTester):
|
|||
self.assertEqual(x1.order, 1)
|
||||
self.assertEqual(x2.order, 0)
|
||||
|
||||
x3 = self.unowned.insert_last('X1')
|
||||
x3 = self.unowned.insert_new('X1')
|
||||
data = {'items': [x3.pk], 'move_to': 0}
|
||||
self.executeBadData(data=data)
|
||||
|
||||
|
@ -365,9 +361,9 @@ class TestRSFormViewset(EndpointTester):
|
|||
response = self.executeOK()
|
||||
self.assertEqual(response.data['id'], self.owned_id)
|
||||
|
||||
x2 = self.owned.insert_last('X2')
|
||||
x1 = self.owned.insert_last('X1')
|
||||
d11 = self.owned.insert_last('D11')
|
||||
x2 = self.owned.insert_new('X2')
|
||||
x1 = self.owned.insert_new('X1')
|
||||
d11 = self.owned.insert_new('D11')
|
||||
|
||||
response = self.executeOK()
|
||||
x1.refresh_from_db()
|
||||
|
@ -387,41 +383,41 @@ class TestRSFormViewset(EndpointTester):
|
|||
def test_load_trs(self):
|
||||
self.set_params(item=self.owned_id)
|
||||
self.owned.model.title = 'Test11'
|
||||
self.owned.model.save()
|
||||
x1 = self.owned.insert_last('X1')
|
||||
self.owned.save()
|
||||
x1 = self.owned.insert_new('X1')
|
||||
work_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
|
||||
data = {'file': file, 'load_metadata': False}
|
||||
response = self.client.patch(self.endpoint, data=data, format='multipart')
|
||||
self.owned.model.refresh_from_db()
|
||||
self.owned.refresh_from_db()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(self.owned.model.title, 'Test11')
|
||||
self.assertEqual(len(response.data['items']), 25)
|
||||
self.assertEqual(self.owned.constituentsQ().count(), 25)
|
||||
self.assertEqual(self.owned.constituents().count(), 25)
|
||||
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())
|
||||
|
||||
|
||||
@decl_endpoint('/api/rsforms/{item}/produce-structure', method='patch')
|
||||
def test_produce_structure(self):
|
||||
self.set_params(item=self.owned_id)
|
||||
x1 = self.owned.insert_last('X1')
|
||||
s1 = self.owned.insert_last(
|
||||
x1 = self.owned.insert_new('X1')
|
||||
s1 = self.owned.insert_new(
|
||||
alias='S1',
|
||||
definition_formal='ℬ(X1×X1)'
|
||||
)
|
||||
s2 = self.owned.insert_last(
|
||||
s2 = self.owned.insert_new(
|
||||
alias='S2',
|
||||
definition_formal='invalid'
|
||||
)
|
||||
s3 = self.owned.insert_last(
|
||||
s3 = self.owned.insert_new(
|
||||
alias='S3',
|
||||
definition_formal='X1×(X1×ℬℬ(X1))×ℬ(X1×X1)'
|
||||
)
|
||||
a1 = self.owned.insert_last(
|
||||
a1 = self.owned.insert_new(
|
||||
alias='A1',
|
||||
definition_formal='1=1'
|
||||
)
|
||||
f1 = self.owned.insert_last(
|
||||
f1 = self.owned.insert_new(
|
||||
alias='F10',
|
||||
definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)'
|
||||
)
|
||||
|
@ -515,7 +511,7 @@ class TestConstituentaAPI(EndpointTester):
|
|||
data = {'target': self.cst1.pk, 'item_data': {'alias': self.cst3.alias}}
|
||||
self.executeBadData(data=data, schema=self.owned_id)
|
||||
|
||||
d1 = self.owned.insert_last(
|
||||
d1 = self.owned.insert_new(
|
||||
alias='D1',
|
||||
term_raw='@{X1|plur}',
|
||||
definition_formal='X1'
|
||||
|
@ -578,19 +574,6 @@ class TestConstituentaAPI(EndpointTester):
|
|||
self.assertEqual(self.cst3.definition_resolved, 'form1')
|
||||
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms'])
|
||||
|
||||
@decl_endpoint('/api/rsforms/{schema}/update-crucial', method='patch')
|
||||
def test_update_crucial(self):
|
||||
data = {'target': [self.cst1.pk], 'value': True}
|
||||
self.executeForbidden(data=data, schema=self.unowned_id)
|
||||
|
||||
self.logout()
|
||||
self.executeForbidden(data=data, schema=self.owned_id)
|
||||
|
||||
self.login()
|
||||
self.executeOK(data=data, schema=self.owned_id)
|
||||
self.cst1.refresh_from_db()
|
||||
self.assertEqual(self.cst1.crucial, True)
|
||||
|
||||
|
||||
class TestInlineSynthesis(EndpointTester):
|
||||
''' Testing Operations endpoints. '''
|
||||
|
@ -629,15 +612,15 @@ class TestInlineSynthesis(EndpointTester):
|
|||
|
||||
|
||||
def test_inline_synthesis(self):
|
||||
ks1_x1 = self.schema1.insert_last('X1', term_raw='KS1X1') # -> delete
|
||||
ks1_x2 = self.schema1.insert_last('X2', term_raw='KS1X2') # -> X2
|
||||
ks1_s1 = self.schema1.insert_last('S1', definition_formal='X2', term_raw='KS1S1') # -> S1
|
||||
ks1_d1 = self.schema1.insert_last('D1', definition_formal=r'S1\X1\X2') # -> D1
|
||||
ks2_x1 = self.schema2.insert_last('X1', term_raw='KS2X1') # -> delete
|
||||
ks2_x2 = self.schema2.insert_last('X2', term_raw='KS2X2') # -> X4
|
||||
ks2_s1 = self.schema2.insert_last('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2
|
||||
ks2_d1 = self.schema2.insert_last('D1', definition_formal=r'S1\X1\X2') # -> D2
|
||||
ks2_a1 = self.schema2.insert_last('A1', definition_formal='1=1') # -> not included in items
|
||||
ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete
|
||||
ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2
|
||||
ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1
|
||||
ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1
|
||||
ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete
|
||||
ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4
|
||||
ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2
|
||||
ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2
|
||||
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items
|
||||
|
||||
data = {
|
||||
'receiver': self.schema1.model.pk,
|
||||
|
|
|
@ -42,7 +42,6 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
'load_trs',
|
||||
'create_cst',
|
||||
'update_cst',
|
||||
'update_crucial',
|
||||
'move_cst',
|
||||
'delete_multiple_cst',
|
||||
'substitute',
|
||||
|
@ -78,7 +77,6 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['post'], url_path='create-cst')
|
||||
def create_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Create Constituenta. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CstCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
@ -86,18 +84,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
insert_after = None
|
||||
else:
|
||||
insert_after = data['insert_after']
|
||||
|
||||
schema = m.RSForm(self._get_item())
|
||||
with transaction.atomic():
|
||||
schema = m.RSFormCached(item)
|
||||
new_cst = schema.create_cst(data, insert_after)
|
||||
PropagationFacade.after_create_cst(schema, [new_cst])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_201_CREATED,
|
||||
data={
|
||||
'new_cst': s.CstInfoSerializer(new_cst).data,
|
||||
'schema': s.RSFormParseSerializer(item).data
|
||||
'schema': s.RSFormParseSerializer(schema.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -115,16 +110,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='update-cst')
|
||||
def update_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update persistent attributes of a given constituenta. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': item})
|
||||
model = self._get_item()
|
||||
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||
schema = m.RSForm(model)
|
||||
data = serializer.validated_data['item_data']
|
||||
|
||||
with transaction.atomic():
|
||||
schema = m.RSFormCached(item)
|
||||
old_data = schema.update_cst(cst.pk, data)
|
||||
PropagationFacade.after_update_cst(schema, cst.pk, data, old_data)
|
||||
old_data = schema.update_cst(cst, data)
|
||||
PropagationFacade.after_update_cst(schema, cst, data, old_data)
|
||||
if 'alias' in data and data['alias'] != cst.alias:
|
||||
cst.refresh_from_db()
|
||||
changed_type = 'cst_type' in data and cst.cst_type != data['cst_type']
|
||||
|
@ -134,42 +128,13 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
cst.cst_type = data['cst_type']
|
||||
cst.save()
|
||||
schema.apply_mapping(mapping=mapping, change_aliases=False)
|
||||
schema.save()
|
||||
cst.refresh_from_db()
|
||||
if changed_type:
|
||||
PropagationFacade.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type))
|
||||
item.save(update_fields=['time_update'])
|
||||
PropagationFacade.after_change_cst_type(schema, cst)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
summary='update crucial attributes of a given list of constituents',
|
||||
tags=['RSForm'],
|
||||
request=s.CrucialUpdateSerializer,
|
||||
responses={
|
||||
c.HTTP_200_OK: s.RSFormParseSerializer,
|
||||
c.HTTP_400_BAD_REQUEST: None,
|
||||
c.HTTP_403_FORBIDDEN: None,
|
||||
c.HTTP_404_NOT_FOUND: None
|
||||
}
|
||||
)
|
||||
@action(detail=True, methods=['patch'], url_path='update-crucial')
|
||||
def update_crucial(self, request: Request, pk) -> HttpResponse:
|
||||
''' Update crucial attributes of a given list of constituents. '''
|
||||
item = self._get_item()
|
||||
serializer = s.CrucialUpdateSerializer(data=request.data, partial=True, context={'schema': item})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
value: bool = serializer.validated_data['value']
|
||||
|
||||
with transaction.atomic():
|
||||
for cst in serializer.validated_data['target']:
|
||||
cst.crucial = value
|
||||
cst.save(update_fields=['crucial'])
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(schema.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -186,9 +151,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='produce-structure')
|
||||
def produce_structure(self, request: Request, pk) -> HttpResponse:
|
||||
''' Produce a term for every element of the target constituenta typification. '''
|
||||
item = self._get_item()
|
||||
model = self._get_item()
|
||||
|
||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': item})
|
||||
serializer = s.CstTargetSerializer(data=request.data, context={'schema': model})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
cst = cast(m.Constituenta, serializer.validated_data['target'])
|
||||
if cst.cst_type not in [m.CstType.FUNCTION, m.CstType.STRUCTURED, m.CstType.TERM]:
|
||||
|
@ -196,27 +161,27 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
f'{cst.pk}': msg.constituentaNoStructure()
|
||||
})
|
||||
|
||||
schema_details = s.RSFormParseSerializer(item).data['items']
|
||||
schema_details = s.RSFormParseSerializer(model).data['items']
|
||||
cst_parse = next(item for item in schema_details if item['id'] == cst.pk)['parse']
|
||||
if not cst_parse['typification']:
|
||||
return Response(
|
||||
status=c.HTTP_400_BAD_REQUEST,
|
||||
data={f'{cst.pk}': msg.constituentaNoStructure()}
|
||||
)
|
||||
schema = m.RSForm(model)
|
||||
|
||||
with transaction.atomic():
|
||||
schema = m.RSFormCached(item)
|
||||
new_cst = schema.produce_structure(cst, cst_parse)
|
||||
PropagationFacade.after_create_cst(schema, new_cst)
|
||||
item.save(update_fields=['time_update'])
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data={
|
||||
'cst_list': [cst.pk for cst in new_cst],
|
||||
'schema': s.RSFormParseSerializer(item).data
|
||||
'schema': s.RSFormParseSerializer(schema.model).data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@extend_schema(
|
||||
summary='execute substitutions',
|
||||
tags=['RSForm'],
|
||||
|
@ -231,27 +196,24 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='substitute')
|
||||
def substitute(self, request: Request, pk) -> HttpResponse:
|
||||
''' Substitute occurrences of constituenta with another one. '''
|
||||
item = self._get_item()
|
||||
model = self._get_item()
|
||||
serializer = s.CstSubstituteSerializer(
|
||||
data=request.data,
|
||||
context={'schema': item}
|
||||
context={'schema': model}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
schema = m.RSForm(model)
|
||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||
|
||||
with transaction.atomic():
|
||||
schema = m.RSForm(item)
|
||||
for substitution in serializer.validated_data['substitutions']:
|
||||
original = cast(m.Constituenta, substitution['original'])
|
||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||
substitutions.append((original, replacement))
|
||||
PropagationFacade.before_substitute(item.pk, substitutions)
|
||||
PropagationFacade.before_substitute(schema, substitutions)
|
||||
schema.substitute(substitutions)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(schema.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -268,23 +230,20 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
|
||||
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Delete multiple Constituents. '''
|
||||
item = self._get_item()
|
||||
model = self._get_item()
|
||||
serializer = s.CstListSerializer(
|
||||
data=request.data,
|
||||
context={'schema': item}
|
||||
context={'schema': model}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
cst_list: list[m.Constituenta] = serializer.validated_data['items']
|
||||
|
||||
schema = m.RSForm(model)
|
||||
with transaction.atomic():
|
||||
schema = m.RSForm(item)
|
||||
PropagationFacade.before_delete_cst(item.pk, [cst.pk for cst in cst_list])
|
||||
PropagationFacade.before_delete_cst(schema, cst_list)
|
||||
schema.delete_cst(cst_list)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(schema.model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -301,24 +260,20 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='move-cst')
|
||||
def move_cst(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Move multiple Constituents. '''
|
||||
item = self._get_item()
|
||||
model = self._get_item()
|
||||
serializer = s.CstMoveSerializer(
|
||||
data=request.data,
|
||||
context={'schema': item}
|
||||
context={'schema': model}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
with transaction.atomic():
|
||||
schema = m.RSForm(item)
|
||||
schema.move_cst(
|
||||
m.RSForm(model).move_cst(
|
||||
target=serializer.validated_data['items'],
|
||||
destination=serializer.validated_data['move_to']
|
||||
)
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -334,16 +289,12 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='reset-aliases')
|
||||
def reset_aliases(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Recreate all aliases based on order. '''
|
||||
item = self._get_item()
|
||||
|
||||
with transaction.atomic():
|
||||
schema = m.RSForm(item)
|
||||
model = self._get_item()
|
||||
schema = m.RSForm(model)
|
||||
schema.reset_aliases()
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -359,15 +310,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
@action(detail=True, methods=['patch'], url_path='restore-order')
|
||||
def restore_order(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Restore order based on types and Term graph. '''
|
||||
item = self._get_item()
|
||||
|
||||
with transaction.atomic():
|
||||
m.OrderManager(m.RSFormCached(item)).restore_order()
|
||||
item.save(update_fields=['time_update'])
|
||||
|
||||
model = self._get_item()
|
||||
m.RSForm(model).restore_order()
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
data=s.RSFormParseSerializer(item).data
|
||||
data=s.RSFormParseSerializer(model).data
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
|
@ -387,7 +334,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
input_serializer = s.RSFormUploadSerializer(data=request.data)
|
||||
input_serializer.is_valid(raise_exception=True)
|
||||
|
||||
item = self._get_item()
|
||||
model = self._get_item()
|
||||
load_metadata = input_serializer.validated_data['load_metadata']
|
||||
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
|
||||
if data is None:
|
||||
|
@ -395,7 +342,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
status=c.HTTP_400_BAD_REQUEST,
|
||||
data={'file': msg.exteorFileCorrupted()}
|
||||
)
|
||||
data['id'] = item.pk
|
||||
data['id'] = model.pk
|
||||
|
||||
serializer = s.RSFormTRSSerializer(
|
||||
data=data,
|
||||
|
@ -459,7 +406,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
serializer = s.ExpressionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
expression = serializer.validated_data['expression']
|
||||
pySchema = s.PyConceptAdapter(pk)
|
||||
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
|
||||
result = pyconcept.check_expression(json.dumps(pySchema.data), expression)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
|
@ -484,7 +431,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
alias = serializer.validated_data['alias']
|
||||
cst_type = cast(m.CstType, serializer.validated_data['cst_type'])
|
||||
|
||||
pySchema = s.PyConceptAdapter(pk)
|
||||
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object()))
|
||||
result = pyconcept.check_constituenta(json.dumps(pySchema.data), alias, expression, cst_type)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
|
@ -506,7 +453,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
serializer = s.TextSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
text = serializer.validated_data['text']
|
||||
resolver = m.RSForm.resolver_from_schema(pk)
|
||||
resolver = m.RSForm(self.get_object()).resolver()
|
||||
resolver.resolve(text)
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
|
@ -526,7 +473,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
|||
def export_trs(self, request: Request, pk) -> HttpResponse:
|
||||
''' Endpoint: Download Exteor compatible file. '''
|
||||
model = self._get_item()
|
||||
data = s.generate_trs(model)
|
||||
data = s.RSFormTRSSerializer(m.RSForm(model)).data
|
||||
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
|
||||
filename = utils.filename_for_schema(model.alias)
|
||||
response = HttpResponse(file, content_type='application/zip')
|
||||
|
@ -646,11 +593,10 @@ def inline_synthesis(request: Request) -> HttpResponse:
|
|||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
receiver = m.RSFormCached(serializer.validated_data['receiver'])
|
||||
receiver = m.RSForm(serializer.validated_data['receiver'])
|
||||
items = cast(list[m.Constituenta], serializer.validated_data['items'])
|
||||
if len(items) == 0:
|
||||
source = cast(LibraryItem, serializer.validated_data['source'])
|
||||
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))
|
||||
items = list(m.RSForm(serializer.validated_data['source']).constituents().order_by('order'))
|
||||
|
||||
with transaction.atomic():
|
||||
new_items = receiver.insert_copy(items)
|
||||
|
@ -668,9 +614,8 @@ def inline_synthesis(request: Request) -> HttpResponse:
|
|||
replacement = new_items[index]
|
||||
substitutions.append((original, replacement))
|
||||
|
||||
PropagationFacade.before_substitute(receiver.model.pk, substitutions)
|
||||
PropagationFacade.before_substitute(receiver, substitutions)
|
||||
receiver.substitute(substitutions)
|
||||
receiver.model.save(update_fields=['time_update'])
|
||||
|
||||
return Response(
|
||||
status=c.HTTP_200_OK,
|
||||
|
|
|
@ -4,10 +4,8 @@ from django.contrib.auth import get_user_model
|
|||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
User = get_user_model()
|
||||
admin.site.unregister(User)
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
''' Admin model: User. '''
|
||||
fieldsets = UserAdmin.fieldsets
|
||||
|
@ -23,3 +21,7 @@ class CustomUserAdmin(UserAdmin):
|
|||
ordering = ['date_joined', 'username']
|
||||
search_fields = ['email', 'first_name', 'last_name', 'username']
|
||||
list_filter = ['is_staff', 'is_superuser', 'is_active']
|
||||
|
||||
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
|
|
|
@ -86,22 +86,10 @@ def operationInputAlreadyConnected():
|
|||
return 'Схема уже подключена к другой операции'
|
||||
|
||||
|
||||
def referenceTypeNotAllowed():
|
||||
return 'Ссылки не поддерживаются'
|
||||
|
||||
|
||||
def referenceTypeRequired():
|
||||
return 'Операция должна быть ссылкой'
|
||||
|
||||
|
||||
def operationNotSynthesis(title: str):
|
||||
return f'Операция не является Синтезом: {title}'
|
||||
|
||||
|
||||
def operationResultEmpty(title: str):
|
||||
return f'Результат операции пуст: {title}'
|
||||
|
||||
|
||||
def operationResultNotEmpty(title: str):
|
||||
return f'Результат операции не пуст: {title}'
|
||||
|
||||
|
@ -142,6 +130,10 @@ def exteorFileVersionNotSupported():
|
|||
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
|
||||
|
||||
|
||||
def invalidPosition():
|
||||
return 'Invalid position: should be positive integer'
|
||||
|
||||
|
||||
def constituentaNoStructure():
|
||||
return 'Указанная конституента не обладает теоретико-множественной типизацией'
|
||||
|
||||
|
|
1776
rsconcept/frontend/package-lock.json
generated
1776
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"generate": "lezer-generator src/features/rsform/components/rs-input/rslang/rslang-fast.grammar -o src/features/rsform/components/rs-input/rslang/parser.ts && lezer-generator src/features/rsform/components/rs-input/rslang/rslang-ast.grammar -o src/features/rsform/components/rs-input/rslang/parser-ast.ts && lezer-generator src/features/rsform/components/refs-input/parse/refs-text.grammar -o src/features/rsform/components/refs-input/parse/parser.ts && lezer-generator src/features/ai/components/prompt-input/parse/prompt-text.grammar -o src/features/ai/components/prompt-input/parse/parser.ts",
|
||||
"generate": "lezer-generator src/features/rsform/components/rs-input/rslang/rslang-fast.grammar -o src/features/rsform/components/rs-input/rslang/parser.ts && lezer-generator src/features/rsform/components/rs-input/rslang/rslang-ast.grammar -o src/features/rsform/components/rs-input/rslang/parser-ast.ts && lezer-generator src/features/rsform/components/refs-input/parse/refs-text.grammar -o src/features/rsform/components/refs-input/parse/parser.ts",
|
||||
"test": "jest",
|
||||
"test:e2e": "playwright test",
|
||||
"dev": "vite --host",
|
||||
|
@ -15,30 +15,30 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.83.0",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query-devtools": "^5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@uiw/codemirror-themes": "^4.24.1",
|
||||
"@uiw/react-codemirror": "^4.24.1",
|
||||
"axios": "^1.11.0",
|
||||
"@uiw/codemirror-themes": "^4.24.0",
|
||||
"@uiw/react-codemirror": "^4.24.0",
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"global": "^4.4.0",
|
||||
"js-file-download": "^0.4.12",
|
||||
"lucide-react": "^0.533.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-intl": "^7.1.11",
|
||||
"react-router": "^7.7.1",
|
||||
"react-router": "^7.6.3",
|
||||
"react-scan": "^0.4.3",
|
||||
"react-tabs": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
|
@ -46,41 +46,41 @@
|
|||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"use-debounce": "^10.0.5",
|
||||
"zod": "^4.0.13",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"@playwright/test": "^1.54.1",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||
"@typescript-eslint/parser": "^8.0.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-playwright": "^2.2.1",
|
||||
"eslint-plugin-playwright": "^2.2.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.0.5",
|
||||
"stylelint": "^16.23.0",
|
||||
"jest": "^30.0.4",
|
||||
"stylelint": "^16.21.1",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-tailwindcss": "^1.0.0",
|
||||
"tailwindcss": "^4.0.7",
|
||||
"ts-jest": "^29.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.6"
|
||||
"typescript-eslint": "^8.36.0",
|
||||
"vite": "^7.0.3"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
|
|
|
@ -3,7 +3,6 @@ import { Outlet } from 'react-router';
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { ModalLoader } from '@/components/modal';
|
||||
import { useBrowserNavigation } from '@/hooks/use-browser-navigation';
|
||||
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
|
@ -17,13 +16,10 @@ import { MutationErrors } from './mutation-errors';
|
|||
import { Navigation } from './navigation';
|
||||
|
||||
export function ApplicationLayout() {
|
||||
useBrowserNavigation();
|
||||
|
||||
const mainHeight = useMainHeight();
|
||||
const viewportHeight = useViewportHeight();
|
||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||
const noNavigation = useAppLayoutStore(state => state.noNavigation);
|
||||
const toastBottom = useAppLayoutStore(state => state.toastBottom);
|
||||
const noFooter = useAppLayoutStore(state => state.noFooter);
|
||||
const activeDialog = useDialogsStore(state => state.active);
|
||||
|
||||
|
@ -36,8 +32,6 @@ export function ApplicationLayout() {
|
|||
autoClose={3000}
|
||||
draggable={false}
|
||||
pauseOnFocusLoss={false}
|
||||
position={toastBottom ? 'bottom-right' : 'top-right'}
|
||||
newestOnTop={toastBottom}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<ModalLoader />}>
|
||||
|
|
|
@ -45,11 +45,6 @@ const DlgDeleteOperation = React.lazy(() =>
|
|||
default: module.DlgDeleteOperation
|
||||
}))
|
||||
);
|
||||
const DlgDeleteReference = React.lazy(() =>
|
||||
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
|
||||
default: module.DlgDeleteReference
|
||||
}))
|
||||
);
|
||||
const DlgEditEditors = React.lazy(() =>
|
||||
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
|
||||
default: module.DlgEditEditors
|
||||
|
@ -201,8 +196,6 @@ export const GlobalDialogs = () => {
|
|||
return <DlgCreateVersion />;
|
||||
case DialogType.DELETE_OPERATION:
|
||||
return <DlgDeleteOperation />;
|
||||
case DialogType.DELETE_REFERENCE:
|
||||
return <DlgDeleteReference />;
|
||||
case DialogType.GRAPH_PARAMETERS:
|
||||
return <DlgGraphParams />;
|
||||
case DialogType.RELOCATE_CONSTITUENTS:
|
||||
|
|
|
@ -3,21 +3,13 @@ import { useDebounce } from 'use-debounce';
|
|||
|
||||
import { Loader } from '@/components/loader';
|
||||
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
|
||||
import { useTransitionTracker } from '@/hooks/use-transition-delay';
|
||||
import { useAppTransitionStore } from '@/stores/app-transition';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
export function GlobalLoader() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const isTransitioning = useTransitionTracker();
|
||||
const isManualNav = useAppTransitionStore(state => state.isNavigating);
|
||||
const isRouterLoading = navigation.state === 'loading';
|
||||
|
||||
const [loadingDebounced] = useDebounce(
|
||||
isRouterLoading || isTransitioning || isManualNav,
|
||||
PARAMETER.navigationPopupDelay
|
||||
);
|
||||
const isLoading = navigation.state === 'loading';
|
||||
const [loadingDebounced] = useDebounce(isLoading, PARAMETER.navigationPopupDelay);
|
||||
|
||||
if (!loadingDebounced) {
|
||||
return null;
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import fileDownload from 'js-file-download';
|
||||
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
import { convertToCSV, convertToJSON } from '@/utils/utils';
|
||||
|
||||
import { Dropdown, DropdownButton, useDropdown } from '../dropdown';
|
||||
import { IconCSV, IconDownload, IconJSON } from '../icons';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { MiniButton } from './mini-button';
|
||||
|
||||
interface ExportDropdownProps<T extends object = object> {
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Data to export (can be readonly or mutable array of objects) */
|
||||
data: readonly Readonly<T>[];
|
||||
|
||||
/** Optional filename (without extension) */
|
||||
|
||||
filename?: string;
|
||||
/** Optional button className */
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ExportDropdown<T extends object = object>({
|
||||
disabled,
|
||||
data,
|
||||
filename = 'export',
|
||||
className
|
||||
}: ExportDropdownProps<T>) {
|
||||
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||
|
||||
function handleExport(format: 'csv' | 'json') {
|
||||
if (!data || data.length === 0) {
|
||||
toast.error(infoMsg.noDataToExport);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (format === 'csv') {
|
||||
const blob = convertToCSV(data);
|
||||
fileDownload(blob, `${filename}.csv`, 'text/csv;charset=utf-8;');
|
||||
} else {
|
||||
const blob = convertToJSON(data);
|
||||
fileDownload(blob, `${filename}.json`, 'application/json;charset=utf-8;');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
hide();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative inline-block', className)} tabIndex={0} onBlur={handleBlur}>
|
||||
<MiniButton
|
||||
title='Экспортировать данные'
|
||||
hideTitle={isOpen}
|
||||
className='text-muted-foreground enabled:hover:text-primary'
|
||||
icon={<IconDownload size='1.25rem' />}
|
||||
onClick={toggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Dropdown ref={ref} isOpen={isOpen} stretchLeft margin='mt-1' className='z-tooltip'>
|
||||
<DropdownButton
|
||||
icon={<IconCSV size='1rem' className='mr-1 icon-green' />}
|
||||
text='CSV'
|
||||
onClick={() => handleExport('csv')}
|
||||
className='w-full justify-start'
|
||||
/>
|
||||
<DropdownButton
|
||||
icon={<IconJSON size='1rem' className='mr-1 icon-green' />}
|
||||
text='JSON'
|
||||
onClick={() => handleExport('json')}
|
||||
className='w-full justify-start'
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type Button } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
|
@ -17,17 +15,7 @@ interface SubmitButtonProps extends Button {
|
|||
/**
|
||||
* Displays submit type button with icon and text.
|
||||
*/
|
||||
export function SubmitButton({
|
||||
text = 'ОК',
|
||||
icon,
|
||||
title,
|
||||
titleHtml,
|
||||
hideTitle,
|
||||
disabled,
|
||||
loading,
|
||||
className,
|
||||
...restProps
|
||||
}: SubmitButtonProps) {
|
||||
export function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type='submit'
|
||||
|
@ -40,10 +28,6 @@ export function SubmitButton({
|
|||
loading && 'cursor-progress',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
disabled={disabled || loading}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { type Button as ButtonStyle } from '../props';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface TextButtonProps extends ButtonStyle {
|
||||
/** Text to display second. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customizable `button` with text, transparent background and no additional styling.
|
||||
*/
|
||||
export function TextButton({ text, title, titleHtml, hideTitle, className, ...restProps }: TextButtonProps) {
|
||||
return (
|
||||
<button
|
||||
tabIndex={-1}
|
||||
type='button'
|
||||
className={cn(
|
||||
'self-start cc-label cc-hover-underline',
|
||||
'font-medium text-primary select-none disabled:text-foreground',
|
||||
'cursor-pointer disabled:cursor-default',
|
||||
'outline-hidden',
|
||||
'select-text',
|
||||
className
|
||||
)}
|
||||
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
|
||||
data-tooltip-html={titleHtml}
|
||||
data-tooltip-content={title}
|
||||
data-tooltip-hidden={hideTitle}
|
||||
aria-label={!text ? title : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
}
|
|
@ -30,7 +30,7 @@ export function TextURL({ text, href, title, color = 'text-primary', onClick }:
|
|||
);
|
||||
} else if (onClick) {
|
||||
return (
|
||||
<button type='button' tabIndex={-1} className={design} title={title} onClick={onClick}>
|
||||
<button type='button' tabIndex={-1} className={design} onClick={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -61,7 +61,6 @@ export { BiLastPage as IconPageLast } from 'react-icons/bi';
|
|||
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
|
||||
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
|
||||
export { PiFileCsv as IconCSV } from 'react-icons/pi';
|
||||
export { BsFiletypeJson as IconJSON } from 'react-icons/bs';
|
||||
|
||||
// ==== User status =======
|
||||
export { LuCircleUserRound as IconUser } from 'react-icons/lu';
|
||||
|
@ -83,7 +82,7 @@ export { TbGps as IconCoordinates } from 'react-icons/tb';
|
|||
export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
|
||||
export { BiDiamond as IconTemplates } from 'react-icons/bi';
|
||||
export { TbHexagons as IconOSS } from 'react-icons/tb';
|
||||
export { MdOutlineSelectAll as IconConceptBlock } from 'react-icons/md';
|
||||
export { BiScreenshot as IconConceptBlock } from 'react-icons/bi';
|
||||
export { TbHexagon as IconRSForm } from 'react-icons/tb';
|
||||
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
|
||||
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
|
||||
|
@ -95,8 +94,6 @@ export { TbHexagonLetterD as IconCstTerm } from 'react-icons/tb';
|
|||
export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb';
|
||||
export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb';
|
||||
export { TbHexagonLetterT as IconCstTheorem } from 'react-icons/tb';
|
||||
export { PiArrowsMergeFill as IconSynthesis } from 'react-icons/pi';
|
||||
export { VscReferences as IconReference } from 'react-icons/vsc';
|
||||
export { LuNewspaper as IconDefinition } from 'react-icons/lu';
|
||||
export { LuDna as IconTerminology } from 'react-icons/lu';
|
||||
export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
|
||||
|
@ -110,7 +107,6 @@ export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
|
|||
export { LuImage as IconImage } from 'react-icons/lu';
|
||||
export { GoVersions as IconVersions } from 'react-icons/go';
|
||||
export { LuAtSign as IconTerm } from 'react-icons/lu';
|
||||
export { MdTaskAlt as IconCrucial } from 'react-icons/md';
|
||||
export { LuSubscript as IconAlias } from 'react-icons/lu';
|
||||
export { TbMathFunction as IconFormula } from 'react-icons/tb';
|
||||
export { BiFontFamily as IconText } from 'react-icons/bi';
|
||||
|
@ -145,17 +141,16 @@ export { BiDuplicate as IconClone } from 'react-icons/bi';
|
|||
export { LuReplace as IconReplace } from 'react-icons/lu';
|
||||
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
|
||||
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
|
||||
export { LuCombine as IconSynthesis } from 'react-icons/lu';
|
||||
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
|
||||
export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu';
|
||||
export { GrConnect as IconConnect } from 'react-icons/gr';
|
||||
export { BiPlayCircle as IconExecute } from 'react-icons/bi';
|
||||
|
||||
// ======== Graph UI =======
|
||||
export { PiFediverseLogo as IconContextSelection } from 'react-icons/pi';
|
||||
export { ImMakeGroup as IconGroupSelection } from 'react-icons/im';
|
||||
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
|
||||
export { BiExpand as IconGraphExpand } from 'react-icons/bi';
|
||||
export { TiArrowMaximise as IconGraphMaximize } from 'react-icons/ti';
|
||||
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu';
|
||||
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
|
||||
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
|
||||
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
|
||||
|
|
|
@ -21,7 +21,6 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
|||
return (
|
||||
<div>
|
||||
<p>Ошибка валидации данных</p>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-base-to-string */}
|
||||
<PrettyJson data={JSON.parse(error.toString()) as unknown} />;
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { type CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
import { describePromptVariable } from '../../labels';
|
||||
import { type PromptVariableType } from '../../models/prompting';
|
||||
|
||||
export function variableCompletions(variables: string[]) {
|
||||
return (context: CompletionContext) => {
|
||||
let word = context.matchBefore(/\{\{[a-zA-Z.-]*/);
|
||||
if (!word && context.explicit) {
|
||||
word = { from: context.pos, to: context.pos, text: '' };
|
||||
}
|
||||
if (!word || (word.from == word.to && !context.explicit)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
from: word.from,
|
||||
to: word.to,
|
||||
options: variables.map(name => ({
|
||||
label: `{{${name}}}`,
|
||||
info: describePromptVariable(name as PromptVariableType)
|
||||
}))
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { PromptInput } from './prompt-input';
|
|
@ -1,66 +0,0 @@
|
|||
import { syntaxTree } from '@codemirror/language';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
||||
|
||||
const invalidVarMark = Decoration.mark({
|
||||
class: 'text-destructive'
|
||||
});
|
||||
|
||||
const validMark = Decoration.mark({
|
||||
class: 'text-(--acc-fg-purple)'
|
||||
});
|
||||
|
||||
class MarkVariablesPlugin {
|
||||
decorations: DecorationSet;
|
||||
allowed: string[];
|
||||
|
||||
constructor(view: EditorView, allowed: string[]) {
|
||||
this.allowed = allowed;
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const tree = syntaxTree(view.state);
|
||||
const doc = view.state.doc;
|
||||
|
||||
tree.iterate({
|
||||
enter: node => {
|
||||
if (node.name === 'Variable') {
|
||||
// Extract inner text from the Variable node ({{my_var}})
|
||||
const text = doc.sliceString(node.from, node.to);
|
||||
const match = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/.exec(text);
|
||||
const varName = match?.[1];
|
||||
|
||||
if (!varName || !this.allowed.includes(varName)) {
|
||||
builder.add(node.from, node.to, invalidVarMark);
|
||||
} else {
|
||||
builder.add(node.from, node.to, validMark);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a ViewPlugin that marks invalid variables in the editor. */
|
||||
export function markVariables(allowed: string[]) {
|
||||
return ViewPlugin.fromClass(
|
||||
class extends MarkVariablesPlugin {
|
||||
constructor(view: EditorView) {
|
||||
super(view, allowed);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: plugin => plugin.decorations
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import { syntaxTree } from '@codemirror/language';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { Decoration, type DecorationSet, type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
||||
|
||||
const noSpellcheckMark = Decoration.mark({
|
||||
attributes: { spellcheck: 'false' }
|
||||
});
|
||||
|
||||
class NoSpellcheckPlugin {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
|
||||
const tree = syntaxTree(view.state);
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
tree.iterate({
|
||||
from,
|
||||
to,
|
||||
enter: node => {
|
||||
if (node.name === 'Variable') {
|
||||
builder.add(node.from, node.to, noSpellcheckMark);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/** Plugin that adds a no-spellcheck attribute to all variables in the editor. */
|
||||
export const noSpellcheckForVariables = ViewPlugin.fromClass(NoSpellcheckPlugin, {
|
||||
decorations: (plugin: NoSpellcheckPlugin) => plugin.decorations
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
import { styleTags, tags } from '@lezer/highlight';
|
||||
|
||||
export const highlighting = styleTags({
|
||||
Variable: tags.name,
|
||||
Error: tags.comment
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import { LRLanguage } from '@codemirror/language';
|
||||
|
||||
import { parser } from './parser';
|
||||
|
||||
export const PromptLanguage = LRLanguage.define({
|
||||
parser: parser,
|
||||
languageData: {}
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const Text = 1,
|
||||
Variable = 2,
|
||||
Error = 3,
|
||||
Filler = 4;
|
|
@ -1,30 +0,0 @@
|
|||
import { printTree } from '@/utils/codemirror';
|
||||
|
||||
import { parser } from './parser';
|
||||
|
||||
const testData = [
|
||||
['', '[Text]'],
|
||||
['тест русский', '[Text[Filler]]'],
|
||||
['test english', '[Text[Filler]]'],
|
||||
['test greek σσσ', '[Text[Filler]]'],
|
||||
['X1 раз два X2', '[Text[Filler]]'],
|
||||
['{{variable}}', '[Text[Variable]]'],
|
||||
['{{var_1}}', '[Text[Variable]]'],
|
||||
['{{user.name}}', '[Text[Variable]]'],
|
||||
['!error!', '[Text[Error]]'],
|
||||
['word !error! word', '[Text[Filler][Error][Filler]]'],
|
||||
['{{variable}} !error! word', '[Text[Variable][Error][Filler]]'],
|
||||
['word {{variable}}', '[Text[Filler][Variable]]'],
|
||||
['word {{variable}} !error!', '[Text[Filler][Variable][Error]]'],
|
||||
['{{variable}} word', '[Text[Variable][Filler]]'],
|
||||
['!err! {{variable}}', '[Text[Error][Variable]]'],
|
||||
['!err! {{variable}} word', '[Text[Error][Variable][Filler]]']
|
||||
] as const;
|
||||
|
||||
/** Test prompt grammar parser with various prompt inputs */
|
||||
describe('Prompt grammar parser', () => {
|
||||
it.each(testData)('Parse %p', (input: string, expectedTree: string) => {
|
||||
const tree = parser.parse(input);
|
||||
expect(printTree(tree)).toBe(expectedTree);
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlighting } from './highlight';
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!vQVQPOOObQQO'#C^OgQPO'#CjO]QPO'#C_OOQO'#C`'#C`OOQO'#Ce'#CeOOQO'#Ca'#CaQVQPOOOuQPO,58xOOQO,59U,59UOzQPO,58yOOQO-E6_-E6_OOQO1G.d1G.dOOQO1G.e1G.e",
|
||||
stateData: '!S~OWOS~OZPO]RO_QO~O[WO~O_QOU^XZ^X]^X~OY[O~O]]O~O_W~',
|
||||
goto: 'x_PP```dPPPjPPPPnTTOVQVORZVTUOVSSOVQXQRYR',
|
||||
nodeNames: '⚠ Text Variable Error Filler',
|
||||
maxTerm: 15,
|
||||
propSources: [highlighting],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData:
|
||||
"(^~RqOX#YXZ#zZ^$o^p#Ypq#zqr&hr!b#Y!c!}&m!}#R#Y#R#S&m#S#T#Y#T#o&m#o#p'v#q#r(R#r#y#Y#y#z$o#z$f#Y$f$g$o$g#BY#Y#BY#BZ$o#BZ$IS#Y$IS$I_$o$I_$I|#Y$I|$JO$o$JO$JT#Y$JT$JU$o$JU$KV#Y$KV$KW$o$KW&FU#Y&FU&FV$o&FV;'S#Y;'S;=`#t<%lO#YP#_V_POX#YZp#Yr!b#Y!c#o#Y#r;'S#Y;'S;=`#t<%lO#YP#wP;=`<%l#Y~$PYW~X^#zpq#z#y#z#z$f$g#z#BY#BZ#z$IS$I_#z$I|$JO#z$JT$JU#z$KV$KW#z&FU&FV#z~$vj_PW~OX#YXZ#zZ^$o^p#Ypq#zr!b#Y!c#o#Y#r#y#Y#y#z$o#z$f#Y$f$g$o$g#BY#Y#BY#BZ$o#BZ$IS#Y$IS$I_$o$I_$I|#Y$I|$JO$o$JO$JT#Y$JT$JU$o$JU$KV#Y$KV$KW$o$KW&FU#Y&FU&FV$o&FV;'S#Y;'S;=`#t<%lO#Y~&mO]~R&t`[Q_POX#YZp#Yr}#Y}!O&m!O!P&m!P!Q#Y!Q![&m![!b#Y!c!}&m!}#R#Y#R#S&m#S#T#Y#T#o&m#r;'S#Y;'S;=`#t<%lO#Y~'yP#o#p'|~(ROZ~~(UP#q#r(X~(^OY~",
|
||||
tokenizers: [0, 1],
|
||||
topRules: { Text: [0, 1] },
|
||||
tokenPrec: 47
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
@precedence {
|
||||
text @right
|
||||
p1
|
||||
p2
|
||||
p3
|
||||
}
|
||||
|
||||
@top Text { textItem* }
|
||||
|
||||
@skip { space }
|
||||
|
||||
@tokens {
|
||||
space { @whitespace+ }
|
||||
variable { $[a-zA-Z_]$[a-zA-Z0-9_.-]* }
|
||||
word { ![@{|}! \t\n]+ }
|
||||
|
||||
@precedence { word, space }
|
||||
}
|
||||
|
||||
textItem {
|
||||
!p1 Variable |
|
||||
!p2 Error |
|
||||
!p3 Filler
|
||||
}
|
||||
|
||||
Filler { word_enum }
|
||||
Error { "!" word_enum "!" }
|
||||
word_enum {
|
||||
word |
|
||||
word !text word_enum
|
||||
}
|
||||
|
||||
Variable {
|
||||
"{{" variable "}}"
|
||||
}
|
||||
|
||||
@detectDelim
|
||||
|
||||
@external propSource highlighting from "./highlight"
|
|
@ -1,144 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { forwardRef, useRef } from 'react';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { type Extension } from '@codemirror/state';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import CodeMirror, {
|
||||
type BasicSetupOptions,
|
||||
type ReactCodeMirrorProps,
|
||||
type ReactCodeMirrorRef
|
||||
} from '@uiw/react-codemirror';
|
||||
import clsx from 'clsx';
|
||||
import { EditorView } from 'codemirror';
|
||||
|
||||
import { Label } from '@/components/input';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
|
||||
import { PromptVariableType } from '../../models/prompting';
|
||||
|
||||
import { variableCompletions } from './completion';
|
||||
import { markVariables } from './mark-variables';
|
||||
import { noSpellcheckForVariables } from './no-spellcheck';
|
||||
import { PromptLanguage } from './parse';
|
||||
import { variableHoverTooltip } from './tooltip';
|
||||
|
||||
const EDITOR_OPTIONS: BasicSetupOptions = {
|
||||
highlightSpecialChars: false,
|
||||
history: true,
|
||||
drawSelection: false,
|
||||
syntaxHighlighting: false,
|
||||
defaultKeymap: true,
|
||||
historyKeymap: true,
|
||||
|
||||
lineNumbers: false,
|
||||
highlightActiveLineGutter: false,
|
||||
foldGutter: false,
|
||||
dropCursor: true,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: false,
|
||||
bracketMatching: false,
|
||||
closeBrackets: false,
|
||||
autocompletion: false,
|
||||
rectangularSelection: false,
|
||||
crosshairCursor: false,
|
||||
highlightActiveLine: false,
|
||||
highlightSelectionMatches: false,
|
||||
closeBracketsKeymap: false,
|
||||
searchKeymap: false,
|
||||
foldKeymap: false,
|
||||
completionKeymap: false,
|
||||
lintKeymap: false
|
||||
};
|
||||
|
||||
interface PromptInputProps
|
||||
extends Pick<
|
||||
ReactCodeMirrorProps,
|
||||
| 'id' //
|
||||
| 'height'
|
||||
| 'minHeight'
|
||||
| 'maxHeight'
|
||||
| 'value'
|
||||
| 'onFocus'
|
||||
| 'onBlur'
|
||||
| 'placeholder'
|
||||
| 'style'
|
||||
| 'className'
|
||||
> {
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
availableVariables?: string[];
|
||||
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
export const PromptInput = forwardRef<ReactCodeMirrorRef, PromptInputProps>(
|
||||
(
|
||||
{
|
||||
id, //
|
||||
label,
|
||||
disabled,
|
||||
onChange,
|
||||
...restProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||
|
||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||
const thisRef = !ref || typeof ref === 'function' ? internalRef : ref;
|
||||
|
||||
const cursor = !disabled ? 'cursor-text' : 'cursor-default';
|
||||
const customTheme: Extension = createTheme({
|
||||
theme: darkMode ? 'dark' : 'light',
|
||||
settings: {
|
||||
fontFamily: 'inherit',
|
||||
background: !disabled ? APP_COLORS.bgInput : APP_COLORS.bgDefault,
|
||||
foreground: APP_COLORS.fgDefault,
|
||||
caret: APP_COLORS.fgDefault
|
||||
},
|
||||
styles: [
|
||||
{ tag: tags.name, cursor: 'default' }, // Variable
|
||||
{ tag: tags.comment, color: APP_COLORS.fgRed } // Error
|
||||
]
|
||||
});
|
||||
|
||||
const variables = restProps.availableVariables ?? Object.values(PromptVariableType);
|
||||
const autoCompleter = autocompletion({
|
||||
override: [variableCompletions(variables)],
|
||||
activateOnTyping: true,
|
||||
icons: false
|
||||
});
|
||||
|
||||
const editorExtensions = [
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({ spellcheck: 'true' }),
|
||||
PromptLanguage,
|
||||
variableHoverTooltip(variables),
|
||||
autoCompleter,
|
||||
noSpellcheckForVariables,
|
||||
markVariables(variables)
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-2', cursor)}>
|
||||
<Label text={label} />
|
||||
<CodeMirror
|
||||
id={id}
|
||||
ref={thisRef}
|
||||
basicSetup={EDITOR_OPTIONS}
|
||||
theme={customTheme}
|
||||
extensions={editorExtensions}
|
||||
indentWithTab={false}
|
||||
onChange={onChange}
|
||||
editable={!disabled}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,87 +0,0 @@
|
|||
import { syntaxTree } from '@codemirror/language';
|
||||
import { type EditorState, type Extension } from '@codemirror/state';
|
||||
import { hoverTooltip, type TooltipView } from '@codemirror/view';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { findEnvelopingNodes } from '@/utils/codemirror';
|
||||
|
||||
import { describePromptVariable } from '../../labels';
|
||||
import { type PromptVariableType } from '../../models/prompting';
|
||||
|
||||
import { Variable } from './parse/parser.terms';
|
||||
|
||||
/**
|
||||
* Retrieves variable from position in Editor.
|
||||
*/
|
||||
function findVariableAt(pos: number, state: EditorState) {
|
||||
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), [Variable]);
|
||||
if (nodes.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
const start = nodes[0].from;
|
||||
const end = nodes[0].to;
|
||||
const text = state.doc.sliceString(start, end);
|
||||
const match = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/.exec(text);
|
||||
const varName = match?.[1];
|
||||
if (!varName) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
varName: varName,
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
}
|
||||
|
||||
const tooltipProducer = (available: string[]) => {
|
||||
return hoverTooltip((view, pos) => {
|
||||
const parse = findVariableAt(pos, view.state);
|
||||
if (!parse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAvailable = available.includes(parse.varName);
|
||||
return {
|
||||
pos: parse.start,
|
||||
end: parse.end,
|
||||
above: false,
|
||||
create: () => domTooltipVariable(parse.varName, isAvailable)
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export function variableHoverTooltip(available: string[]): Extension {
|
||||
return [tooltipProducer(available)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DOM tooltip for {@link PromptVariableType}.
|
||||
*/
|
||||
function domTooltipVariable(varName: string, isAvailable: boolean): TooltipView {
|
||||
const dom = document.createElement('div');
|
||||
dom.className = clsx(
|
||||
'max-h-100 max-w-100 min-w-40',
|
||||
'dense',
|
||||
'p-2 flex flex-col',
|
||||
'rounded-md shadow-md',
|
||||
'cc-scroll-y',
|
||||
'text-sm bg-card',
|
||||
'select-none cursor-auto'
|
||||
);
|
||||
|
||||
const header = document.createElement('p');
|
||||
header.innerHTML = `<b>Переменная ${varName}</b>`;
|
||||
dom.appendChild(header);
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = isAvailable ? 'text-green-700' : 'text-red-700';
|
||||
status.innerText = isAvailable ? 'Доступна для использования' : 'Недоступна для использования';
|
||||
dom.appendChild(status);
|
||||
|
||||
const desc = document.createElement('p');
|
||||
desc.className = '';
|
||||
desc.innerText = `Описание: ${describePromptVariable(varName as PromptVariableType)}`;
|
||||
dom.appendChild(desc);
|
||||
|
||||
return { dom: dom };
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||
|
||||
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||
import { PromptVariableType } from '../../models/prompting';
|
||||
import { extractPromptVariables } from '../../models/prompting-api';
|
||||
import { evaluatePromptVariable, useAIStore } from '../../stores/ai-context';
|
||||
|
||||
import { MenuAIPrompt } from './menu-ai-prompt';
|
||||
import { TabPromptEdit } from './tab-prompt-edit';
|
||||
import { TabPromptResult } from './tab-prompt-result';
|
||||
import { TabPromptVariables } from './tab-prompt-variables';
|
||||
|
||||
interface AIPromptTabsProps {
|
||||
promptID: number;
|
||||
activeTab: number;
|
||||
setActiveTab: (value: TabID) => void;
|
||||
}
|
||||
|
||||
export const TabID = {
|
||||
TEMPLATE: 0,
|
||||
RESULT: 1,
|
||||
VARIABLES: 2
|
||||
} as const;
|
||||
type TabID = (typeof TabID)[keyof typeof TabID];
|
||||
|
||||
export function AIPromptTabs({ promptID, activeTab, setActiveTab }: AIPromptTabsProps) {
|
||||
const context = useAIStore();
|
||||
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||
const [text, setText] = useState(promptTemplate.text);
|
||||
const variables = extractPromptVariables(text);
|
||||
|
||||
const generatedPrompt = (() => {
|
||||
let result = text;
|
||||
for (const variable of variables) {
|
||||
const type = Object.values(PromptVariableType).find(t => t === variable);
|
||||
let value = '';
|
||||
if (type) {
|
||||
value = evaluatePromptVariable(type, context) ?? '';
|
||||
}
|
||||
result = result.replace(new RegExp(`\{\{${variable}\}\}`, 'g'), value || `${variable}`);
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
setText(promptTemplate.text);
|
||||
}, [promptTemplate]);
|
||||
|
||||
return (
|
||||
<Tabs selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
||||
<TabList className='mx-auto w-fit flex border-x border-b divide-x rounded-none'>
|
||||
<MenuAIPrompt promptID={promptID} generatedPrompt={generatedPrompt} />
|
||||
|
||||
<TabLabel label='Шаблон' />
|
||||
<TabLabel label='Результат' />
|
||||
<TabLabel label='Переменные' />
|
||||
</TabList>
|
||||
|
||||
<div className='h-80 flex flex-col gap-2'>
|
||||
<TabPanel>
|
||||
<TabPromptEdit
|
||||
text={text}
|
||||
setText={setText}
|
||||
label={promptTemplate.label}
|
||||
description={promptTemplate.description}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<TabPromptResult prompt={generatedPrompt} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<TabPromptVariables template={text} />
|
||||
</TabPanel>
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
|
@ -1,26 +1,38 @@
|
|||
import { Suspense, useState } from 'react';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
|
||||
import { ComboBox } from '@/components/input/combo-box';
|
||||
import { Loader } from '@/components/loader';
|
||||
import { ModalView } from '@/components/modal';
|
||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||
|
||||
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
|
||||
|
||||
import { AIPromptTabs, TabID } from './ai-prompt-tabs';
|
||||
import { TabPromptResult } from './tab-prompt-result';
|
||||
import { TabPromptSelect } from './tab-prompt-select';
|
||||
import { TabPromptVariables } from './tab-prompt-variables';
|
||||
|
||||
export const TabID = {
|
||||
SELECT: 0,
|
||||
RESULT: 1,
|
||||
VARIABLES: 2
|
||||
} as const;
|
||||
type TabID = (typeof TabID)[keyof typeof TabID];
|
||||
|
||||
export function DlgAIPromptDialog() {
|
||||
const [activeTab, setActiveTab] = useState<number>(TabID.TEMPLATE);
|
||||
const [activeTab, setActiveTab] = useState<TabID>(TabID.SELECT);
|
||||
const [selected, setSelected] = useState<number | null>(null);
|
||||
const { items: prompts } = useAvailableTemplatesSuspense();
|
||||
|
||||
return (
|
||||
<ModalView
|
||||
header='Генератор запросом LLM'
|
||||
className='w-100 sm:w-160 px-6 flex flex-col h-120'
|
||||
helpTopic={HelpTopic.ASSISTANT}
|
||||
>
|
||||
<ModalView header='Генератор запросом LLM' className='w-100 sm:w-160 px-6'>
|
||||
<Tabs selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
|
||||
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'>
|
||||
<TabLabel label='Шаблон' />
|
||||
<TabLabel label='Результат' disabled={!selected} />
|
||||
<TabLabel label='Переменные' disabled={!selected} />
|
||||
</TabList>
|
||||
|
||||
<div className='h-120 flex flex-col gap-2'>
|
||||
<ComboBox
|
||||
id='prompt-select'
|
||||
items={prompts}
|
||||
|
@ -33,8 +45,12 @@ export function DlgAIPromptDialog() {
|
|||
className='w-full'
|
||||
/>
|
||||
<Suspense fallback={<Loader />}>
|
||||
{selected ? <AIPromptTabs promptID={selected} activeTab={activeTab} setActiveTab={setActiveTab} /> : null}
|
||||
<TabPanel>{selected ? <TabPromptSelect promptID={selected} /> : null}</TabPanel>
|
||||
<TabPanel>{selected ? <TabPromptResult promptID={selected} /> : null}</TabPanel>
|
||||
<TabPanel>{selected ? <TabPromptVariables promptID={selected} /> : null}</TabPanel>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Tabs>
|
||||
</ModalView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClone, IconEdit2 } from '@/components/icons';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
import { PromptTabID } from '../../pages/prompt-templates-page/templates-tabs';
|
||||
|
||||
interface MenuAIPromptProps {
|
||||
promptID: number;
|
||||
generatedPrompt: string;
|
||||
}
|
||||
|
||||
export function MenuAIPrompt({ promptID, generatedPrompt }: MenuAIPromptProps) {
|
||||
const router = useConceptNavigation();
|
||||
const hideDialog = useDialogsStore(state => state.hideDialog);
|
||||
|
||||
function navigatePrompt() {
|
||||
hideDialog();
|
||||
router.push({ path: urls.prompt_template(promptID, PromptTabID.EDIT) });
|
||||
}
|
||||
|
||||
function handleCopyPrompt() {
|
||||
void navigator.clipboard.writeText(generatedPrompt);
|
||||
toast.success(infoMsg.promptReady);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex border-r-2 pr-2'>
|
||||
<MiniButton
|
||||
title='Редактировать шаблон'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconEdit2 size='1.25rem' />}
|
||||
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
|
||||
onClick={navigatePrompt}
|
||||
/>
|
||||
<MiniButton
|
||||
title='Скопировать результат в буфер обмена'
|
||||
noHover
|
||||
noPadding
|
||||
icon={<IconClone size='1.25rem' />}
|
||||
className='h-full pl-2 text-muted-foreground hover:text-constructive cc-animate-color bg-transparent'
|
||||
onClick={handleCopyPrompt}
|
||||
disabled={!generatedPrompt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { TextArea } from '@/components/input';
|
||||
|
||||
import { PromptInput } from '../../components/prompt-input';
|
||||
import { useAvailableVariables } from '../../stores/use-available-variables';
|
||||
|
||||
interface TabPromptEditProps {
|
||||
label: string;
|
||||
description: string;
|
||||
text: string;
|
||||
setText: (value: string) => void;
|
||||
}
|
||||
|
||||
export function TabPromptEdit({ label, description, text, setText }: TabPromptEditProps) {
|
||||
const availableVariables = useAvailableVariables();
|
||||
return (
|
||||
<div className='cc-column'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<TextArea
|
||||
id='prompt-label'
|
||||
label='Название' //
|
||||
value={label}
|
||||
disabled
|
||||
noResize
|
||||
rows={1}
|
||||
/>
|
||||
<TextArea id='prompt-description' label='Описание' value={description} disabled noResize rows={3} />
|
||||
<PromptInput
|
||||
id='prompt-text' //
|
||||
label='Текст шаблона'
|
||||
value={text}
|
||||
onChange={setText}
|
||||
maxHeight='10rem'
|
||||
availableVariables={availableVariables}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,17 +1,65 @@
|
|||
import { useMemo } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
import { IconClone } from '@/components/icons';
|
||||
import { TextArea } from '@/components/input';
|
||||
import { infoMsg } from '@/utils/labels';
|
||||
|
||||
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||
import { PromptVariableType } from '../../models/prompting';
|
||||
import { extractPromptVariables } from '../../models/prompting-api';
|
||||
import { evaluatePromptVariable, useAIStore } from '../../stores/ai-context';
|
||||
|
||||
interface TabPromptResultProps {
|
||||
prompt: string;
|
||||
promptID: number;
|
||||
}
|
||||
|
||||
export function TabPromptResult({ prompt }: TabPromptResultProps) {
|
||||
export function TabPromptResult({ promptID }: TabPromptResultProps) {
|
||||
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||
const context = useAIStore();
|
||||
const variables = useMemo(() => {
|
||||
return promptTemplate ? extractPromptVariables(promptTemplate.text) : [];
|
||||
}, [promptTemplate]);
|
||||
|
||||
const generatedMessage = (() => {
|
||||
if (!promptTemplate) {
|
||||
return '';
|
||||
}
|
||||
let result = promptTemplate.text;
|
||||
for (const variable of variables) {
|
||||
const type = Object.values(PromptVariableType).find(t => t === variable);
|
||||
let value = '';
|
||||
if (type) {
|
||||
value = evaluatePromptVariable(type, context) ?? '';
|
||||
}
|
||||
result = result.replace(new RegExp(`\{\{${variable}\}\}`, 'g'), value || `${variable}`);
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
function handleCopyPrompt() {
|
||||
void navigator.clipboard.writeText(generatedMessage);
|
||||
toast.success(infoMsg.promptReady);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<MiniButton
|
||||
title='Скопировать в буфер обмена'
|
||||
className='absolute -top-23 left-0'
|
||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||
onClick={handleCopyPrompt}
|
||||
disabled={!generatedMessage}
|
||||
/>
|
||||
<TextArea
|
||||
aria-label='Сгенерированное сообщение'
|
||||
value={prompt}
|
||||
value={generatedMessage}
|
||||
placeholder='Текст шаблона пуст'
|
||||
disabled
|
||||
className='w-full h-100'
|
||||
fitContent
|
||||
className='w-full max-h-100 min-h-12'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import { TextArea } from '@/components/input';
|
||||
|
||||
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||
|
||||
interface TabPromptSelectProps {
|
||||
promptID: number;
|
||||
}
|
||||
|
||||
export function TabPromptSelect({ promptID }: TabPromptSelectProps) {
|
||||
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||
|
||||
return (
|
||||
<div className='cc-column'>
|
||||
{promptTemplate && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<TextArea
|
||||
id='prompt-label'
|
||||
label='Название' //
|
||||
value={promptTemplate.label}
|
||||
disabled
|
||||
noResize
|
||||
rows={1}
|
||||
/>
|
||||
<TextArea
|
||||
id='prompt-description'
|
||||
label='Описание'
|
||||
value={promptTemplate.description}
|
||||
disabled
|
||||
noResize
|
||||
rows={3}
|
||||
/>
|
||||
<TextArea
|
||||
id='prompt-text' //
|
||||
label='Текст шаблона'
|
||||
value={promptTemplate.text}
|
||||
disabled
|
||||
noResize
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,16 +1,18 @@
|
|||
'use client';
|
||||
|
||||
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
|
||||
import { describePromptVariable } from '../../labels';
|
||||
import { PromptVariableType } from '../../models/prompting';
|
||||
import { extractPromptVariables } from '../../models/prompting-api';
|
||||
import { useAvailableVariables } from '../../stores/use-available-variables';
|
||||
|
||||
interface TabPromptVariablesProps {
|
||||
template: string;
|
||||
promptID: number;
|
||||
}
|
||||
|
||||
export function TabPromptVariables({ template }: TabPromptVariablesProps) {
|
||||
const variables = extractPromptVariables(template);
|
||||
export function TabPromptVariables({ promptID }: TabPromptVariablesProps) {
|
||||
const { promptTemplate } = usePromptTemplateSuspense(promptID);
|
||||
const variables = extractPromptVariables(promptTemplate.text);
|
||||
const availableTypes = useAvailableVariables();
|
||||
return (
|
||||
<ul>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Controller, useForm, useWatch } from 'react-hook-form';
|
|||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
import { HelpTopic } from '@/features/help';
|
||||
|
||||
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||
import { ModalForm } from '@/components/modal';
|
||||
|
@ -53,7 +52,6 @@ export function DlgCreatePromptTemplate() {
|
|||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||
submitInvalidTooltip='Введите уникальное название шаблона'
|
||||
className='cc-column w-140 max-h-120 py-2 px-6'
|
||||
helpTopic={HelpTopic.ASSISTANT}
|
||||
>
|
||||
<TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' error={errors.label} />
|
||||
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' error={errors.description} />
|
||||
|
|
|
@ -4,22 +4,14 @@ const describePromptVariableRecord: Record<PromptVariableType, string> = {
|
|||
[PromptVariableType.BLOCK]: 'Текущий блок операционной схемы',
|
||||
[PromptVariableType.OSS]: 'Текущая операционная схема',
|
||||
[PromptVariableType.SCHEMA]: 'Текущая концептуальная схема',
|
||||
[PromptVariableType.SCHEMA_THESAURUS]: 'Термины и определения текущей концептуальной схемы',
|
||||
[PromptVariableType.SCHEMA_GRAPH]: 'Граф связей определений конституент',
|
||||
[PromptVariableType.SCHEMA_TYPE_GRAPH]: 'Граф ступеней концептуальной схемы',
|
||||
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента',
|
||||
[PromptVariableType.CONSTITUENTA_SYNTAX_TREE]: 'Синтаксическое дерево конституенты'
|
||||
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента'
|
||||
};
|
||||
|
||||
const mockPromptVariableRecord: Record<PromptVariableType, string> = {
|
||||
[PromptVariableType.BLOCK]: 'Пример: Текущий блок операционной схемы',
|
||||
[PromptVariableType.OSS]: 'Пример: Текущая операционная схема',
|
||||
[PromptVariableType.SCHEMA]: 'Пример: Текущая концептуальная схема',
|
||||
[PromptVariableType.SCHEMA_THESAURUS]: 'Пример\nТермин1 - Определение1\nТермин2 - Определение2',
|
||||
[PromptVariableType.SCHEMA_GRAPH]: 'Пример: Граф связей определений конституент',
|
||||
[PromptVariableType.SCHEMA_TYPE_GRAPH]: 'Пример: Граф ступеней концептуальной схемы',
|
||||
[PromptVariableType.CONSTITUENTA]: 'Пример: Текущая конституента',
|
||||
[PromptVariableType.CONSTITUENTA_SYNTAX_TREE]: 'Пример синтаксического дерева конституенты'
|
||||
[PromptVariableType.CONSTITUENTA]: 'Пример: Текущая конституента'
|
||||
};
|
||||
|
||||
/** Retrieves description for {@link PromptVariableType}. */
|
||||
|
|
|
@ -1,11 +1,3 @@
|
|||
import { type IBlock, type IOperationSchema, NodeType } from '@/features/oss/models/oss';
|
||||
import { CstType, type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||
import { labelCstTypification } from '@/features/rsform/labels';
|
||||
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
|
||||
import { TypificationGraph } from '@/features/rsform/models/typification-graph';
|
||||
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { mockPromptVariable } from '../labels';
|
||||
|
||||
/** Extracts a list of variables (as string[]) from a target string.
|
||||
|
@ -35,125 +27,3 @@ export function generateSample(target: string): string {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a schema variable. */
|
||||
export function varSchema(schema: IRSForm): string {
|
||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||
result += 'Конституенты:\n';
|
||||
schema.items.forEach(item => {
|
||||
result += `\n${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
|
||||
item.definition_formal
|
||||
}" - "${item.definition_resolved}" - "${item.convention}"`;
|
||||
});
|
||||
if (schema.stats.count_crucial > 0) {
|
||||
result +=
|
||||
'\nКлючевые конституенты: ' +
|
||||
schema.items
|
||||
.filter(cst => cst.crucial)
|
||||
.map(cst => cst.alias)
|
||||
.join(', ');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a schema thesaurus variable. */
|
||||
export function varSchemaThesaurus(schema: IRSForm): string {
|
||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||
result += 'Термины:\n';
|
||||
schema.items.forEach(item => {
|
||||
if (item.cst_type === CstType.AXIOM || item.cst_type === CstType.THEOREM) {
|
||||
return;
|
||||
}
|
||||
if (isBasicConcept(item.cst_type)) {
|
||||
result += `\n${item.term_resolved} - "${item.convention}"`;
|
||||
} else {
|
||||
result += `\n${item.term_resolved} - "${item.definition_resolved}"`;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a schema graph variable. */
|
||||
export function varSchemaGraph(schema: IRSForm): string {
|
||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||
result += 'Узлы графа\n';
|
||||
result += JSON.stringify(schema.items, null, PARAMETER.indentJSON);
|
||||
result += '\n\nСвязи графа';
|
||||
schema.graph.nodes.forEach(node => (result += `\n${node.id} -> ${node.outputs.join(', ')}`));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a schema type graph variable. */
|
||||
export function varSchemaTypeGraph(schema: IRSForm): string {
|
||||
const graph = new TypificationGraph();
|
||||
schema.items.forEach(item => graph.addConstituenta(item.alias, item.parse.typification, item.parse.args));
|
||||
|
||||
let result = `Название концептуальной схемы: ${schema.title}\n`;
|
||||
result += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||
result += 'Ступени\n';
|
||||
result += JSON.stringify(graph.nodes, null, PARAMETER.indentJSON);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a OSS variable. */
|
||||
export function varOSS(oss: IOperationSchema): string {
|
||||
let result = `Название операционной схемы: ${oss.title}\n`;
|
||||
result += `Сокращение: ${oss.alias}\n`;
|
||||
result += `Описание: ${oss.description}\n`;
|
||||
result += `Блоки: ${oss.blocks.length}\n`;
|
||||
oss.hierarchy.topologicalOrder().forEach(blockID => {
|
||||
const block = oss.itemByNodeID.get(blockID);
|
||||
if (block?.nodeType !== NodeType.BLOCK) {
|
||||
return;
|
||||
}
|
||||
result += `\nБлок ${block.id}: ${block.title}\n`;
|
||||
result += `Описание: ${block.description}\n`;
|
||||
result += `Предок: "${block.parent}"\n`;
|
||||
});
|
||||
result += `Операции: ${oss.operations.length}\n`;
|
||||
oss.operations.forEach(operation => {
|
||||
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
|
||||
result += `Название: ${operation.title}\n`;
|
||||
result += `Описание: ${operation.description}\n`;
|
||||
result += `Блок: ${operation.parent}`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a block variable. */
|
||||
export function varBlock(target: IBlock, oss: IOperationSchema): string {
|
||||
const blocks = oss.blocks.filter(block => block.parent === target.id);
|
||||
const operations = oss.operations.filter(operation => operation.parent === target.id);
|
||||
let result = `Название блока: ${target.title}\n`;
|
||||
result += `Описание: "${target.description}"\n`;
|
||||
result += '\nСодержание\n';
|
||||
result += `Блоки: ${blocks.length}\n`;
|
||||
blocks.forEach(block => {
|
||||
result += `\nБлок ${block.id}: ${block.title}\n`;
|
||||
result += `Описание: "${block.description}"\n`;
|
||||
});
|
||||
result += `Операции: ${operations.length}\n`;
|
||||
operations.forEach(operation => {
|
||||
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
|
||||
result += `Название: "${operation.title}"\n`;
|
||||
result += `Описание: "${operation.description}"`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Generates a prompt for a constituenta variable. */
|
||||
export function varConstituenta(cst: IConstituenta): string {
|
||||
return JSON.stringify(cst, null, PARAMETER.indentJSON);
|
||||
}
|
||||
|
||||
/** Generates a prompt for a constituenta syntax tree variable. */
|
||||
export function varSyntaxTree(cst: IConstituenta): string {
|
||||
let result = `Конституента: ${cst.alias}\n`;
|
||||
result += `Формальное выражение: ${cst.definition_formal}\n`;
|
||||
result += `Дерево синтаксического разбора:\n`;
|
||||
result += JSON.stringify(cst.parse.syntaxTree, null, PARAMETER.indentJSON);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
/** Represents prompt variable type. */
|
||||
export const PromptVariableType = {
|
||||
SCHEMA: 'schema',
|
||||
SCHEMA_THESAURUS: 'schema.thesaurus',
|
||||
SCHEMA_GRAPH: 'schema.graph',
|
||||
SCHEMA_TYPE_GRAPH: 'schema.type-graph',
|
||||
|
||||
CONSTITUENTA: 'constituenta',
|
||||
CONSTITUENTA_SYNTAX_TREE: 'constituenta.ast',
|
||||
BLOCK: 'block',
|
||||
// BLOCK_TITLE: 'block.title',
|
||||
// BLOCK_DESCRIPTION: 'block.description',
|
||||
// BLOCK_CONTENTS: 'block.contents',
|
||||
|
||||
OSS: 'oss',
|
||||
// OSS_CONTENTS: 'oss.contents',
|
||||
// OSS_ALIAS: 'oss.alias',
|
||||
// OSS_TITLE: 'oss.title',
|
||||
// OSS_DESCRIPTION: 'oss.description',
|
||||
|
||||
BLOCK: 'block'
|
||||
SCHEMA: 'schema',
|
||||
// SCHEMA_ALIAS: 'schema.alias',
|
||||
// SCHEMA_TITLE: 'schema.title',
|
||||
// SCHEMA_DESCRIPTION: 'schema.description',
|
||||
// SCHEMA_THESAURUS: 'schema.thesaurus',
|
||||
// SCHEMA_GRAPH: 'schema.graph',
|
||||
// SCHEMA_TYPE_GRAPH: 'schema.type-graph',
|
||||
|
||||
CONSTITUENTA: 'constituenta'
|
||||
// CONSTITUENTA_ALIAS: 'constituent.alias',
|
||||
// CONSTITUENTA_CONVENTION: 'constituent.convention',
|
||||
// CONSTITUENTA_DEFINITION: 'constituent.definition',
|
||||
// CONSTITUENTA_DEFINITION_FORMAL: 'constituent.definition-formal',
|
||||
// CONSTITUENTA_EXPRESSION_TREE: 'constituent.expression-tree'
|
||||
} as const;
|
||||
export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType];
|
||||
|
|
|
@ -14,7 +14,7 @@ import { useModificationStore } from '@/stores/modification';
|
|||
import { PromptTabID, TemplatesTabs } from './templates-tabs';
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
tab: z.preprocess(v => (v ? Number(v) : undefined), z.enum(PromptTabID).default(PromptTabID.LIST)),
|
||||
tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(PromptTabID).default(PromptTabID.LIST)),
|
||||
active: z.preprocess(v => (v ? Number(v) : undefined), z.number().nullable().default(null))
|
||||
});
|
||||
|
||||
|
@ -26,7 +26,7 @@ export function PromptTemplatesPage() {
|
|||
active: query.get('active')
|
||||
});
|
||||
|
||||
const isModified = useModificationStore(state => state.isModified);
|
||||
const { isModified } = useModificationStore();
|
||||
useBlockNavigation(isModified);
|
||||
|
||||
return (
|
||||
|
|
|
@ -7,7 +7,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||
import clsx from 'clsx';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { PromptInput } from '@/features/ai/components/prompt-input';
|
||||
import { useAuthSuspense } from '@/features/auth';
|
||||
|
||||
import { MiniButton } from '@/components/control';
|
||||
|
@ -89,11 +88,6 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
|
|||
});
|
||||
}
|
||||
|
||||
function handleChangeText(newValue: string, onChange: (newValue: string) => void) {
|
||||
setSampleResult(null);
|
||||
onChange(newValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
id={globalIDs.prompt_editor}
|
||||
|
@ -114,22 +108,15 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
|
|||
error={errors.description}
|
||||
disabled={isProcessing || !isMutable}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name='text'
|
||||
render={({ field }) => (
|
||||
<PromptInput
|
||||
<TextArea
|
||||
id='prompt_text'
|
||||
label='Содержание'
|
||||
placeholder='Пример: Предложи дополнение для КС {{schema}}'
|
||||
label='Содержание' //
|
||||
fitContent
|
||||
className='disabled:min-h-9 max-h-64'
|
||||
value={field.value}
|
||||
onChange={newValue => handleChangeText(newValue, field.onChange)}
|
||||
{...register('text')}
|
||||
error={errors.text}
|
||||
disabled={isProcessing || !isMutable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className='flex justify-between'>
|
||||
<Controller
|
||||
name='is_shared'
|
||||
|
@ -150,7 +137,6 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
|
|||
title='Сгенерировать пример запроса'
|
||||
icon={<IconSample size='1.25rem' className='icon-primary' />}
|
||||
onClick={() => setSampleResult(!!sampleResult ? null : generateSample(text))}
|
||||
disabled={!text}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { useFitHeight } from '@/stores/app-layout';
|
||||
|
||||
import { describePromptVariable } from '../../labels';
|
||||
import { PromptVariableType } from '../../models/prompting';
|
||||
|
||||
/** Displays all prompt variable types with their descriptions. */
|
||||
export function TabViewVariables() {
|
||||
const panelHeight = useFitHeight('3rem');
|
||||
|
||||
return (
|
||||
<div className='mt-10 cc-scroll-y min-h-40' style={{ maxHeight: panelHeight }}>
|
||||
<div className='pt-8'>
|
||||
<ul className='space-y-1'>
|
||||
{Object.values(PromptVariableType).map(variableType => (
|
||||
<li key={variableType} className='flex flex-col'>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { Suspense } from 'react';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { HelpTopic } from '@/features/help';
|
||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||
|
||||
import { Loader } from '@/components/loader';
|
||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||
|
@ -61,9 +59,6 @@ export function TemplatesTabs({ activeID, tab }: TemplatesTabsProps) {
|
|||
<TabLabel label='Список' />
|
||||
<TabLabel label='Шаблон' />
|
||||
<TabLabel label='Переменные' />
|
||||
<div className='flex px-1'>
|
||||
<BadgeHelp topic={HelpTopic.ASSISTANT} offset={5} />
|
||||
</div>
|
||||
</TabList>
|
||||
<div className='overflow-x-hidden'>
|
||||
<TabPanel>
|
||||
|
|
|
@ -1,19 +1,10 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
import { type IBlock, type IOperation, type IOperationSchema } from '@/features/oss/models/oss';
|
||||
import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss';
|
||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||
import { labelCstTypification } from '@/features/rsform/labels';
|
||||
|
||||
import { PromptVariableType } from '../models/prompting';
|
||||
import {
|
||||
varBlock,
|
||||
varConstituenta,
|
||||
varOSS,
|
||||
varSchema,
|
||||
varSchemaGraph,
|
||||
varSchemaThesaurus,
|
||||
varSchemaTypeGraph,
|
||||
varSyntaxTree
|
||||
} from '../models/prompting-api';
|
||||
|
||||
interface AIContextStore {
|
||||
currentOSS: IOperationSchema | null;
|
||||
|
@ -25,9 +16,6 @@ interface AIContextStore {
|
|||
currentBlock: IBlock | null;
|
||||
setCurrentBlock: (value: IBlock | null) => void;
|
||||
|
||||
currentOperation: IOperation | null;
|
||||
setCurrentOperation: (value: IOperation | null) => void;
|
||||
|
||||
currentConstituenta: IConstituenta | null;
|
||||
setCurrentConstituenta: (value: IConstituenta | null) => void;
|
||||
}
|
||||
|
@ -42,9 +30,6 @@ export const useAIStore = create<AIContextStore>()(set => ({
|
|||
currentBlock: null,
|
||||
setCurrentBlock: value => set({ currentBlock: value }),
|
||||
|
||||
currentOperation: null,
|
||||
setCurrentOperation: value => set({ currentOperation: value }),
|
||||
|
||||
currentConstituenta: null,
|
||||
setCurrentConstituenta: value => set({ currentConstituenta: value })
|
||||
}));
|
||||
|
@ -55,12 +40,10 @@ export function makeVariableSelector(variableType: PromptVariableType) {
|
|||
case PromptVariableType.OSS:
|
||||
return (state: AIContextStore) => ({ currentOSS: state.currentOSS });
|
||||
case PromptVariableType.SCHEMA:
|
||||
case PromptVariableType.SCHEMA_THESAURUS:
|
||||
return (state: AIContextStore) => ({ currentSchema: state.currentSchema });
|
||||
case PromptVariableType.BLOCK:
|
||||
return (state: AIContextStore) => ({ currentBlock: state.currentBlock, currentOSS: state.currentOSS });
|
||||
return (state: AIContextStore) => ({ currentBlock: state.currentBlock });
|
||||
case PromptVariableType.CONSTITUENTA:
|
||||
case PromptVariableType.CONSTITUENTA_SYNTAX_TREE:
|
||||
return (state: AIContextStore) => ({ currentConstituenta: state.currentConstituenta });
|
||||
default:
|
||||
return () => ({});
|
||||
|
@ -71,22 +54,25 @@ export function makeVariableSelector(variableType: PromptVariableType) {
|
|||
export function evaluatePromptVariable(variableType: PromptVariableType, context: Partial<AIContextStore>): string {
|
||||
switch (variableType) {
|
||||
case PromptVariableType.OSS:
|
||||
return context.currentOSS ? varOSS(context.currentOSS) : `!${variableType}!`;
|
||||
return context.currentOSS?.title ?? '';
|
||||
case PromptVariableType.SCHEMA:
|
||||
return context.currentSchema ? varSchema(context.currentSchema) : `!${variableType}!`;
|
||||
case PromptVariableType.SCHEMA_THESAURUS:
|
||||
return context.currentSchema ? varSchemaThesaurus(context.currentSchema) : `!${variableType}!`;
|
||||
case PromptVariableType.SCHEMA_GRAPH:
|
||||
return context.currentSchema ? varSchemaGraph(context.currentSchema) : `!${variableType}!`;
|
||||
case PromptVariableType.SCHEMA_TYPE_GRAPH:
|
||||
return context.currentSchema ? varSchemaTypeGraph(context.currentSchema) : `!${variableType}!`;
|
||||
return context.currentSchema ? generateSchemaPrompt(context.currentSchema) : '';
|
||||
case PromptVariableType.BLOCK:
|
||||
return context.currentBlock && context.currentOSS
|
||||
? varBlock(context.currentBlock, context.currentOSS)
|
||||
: `!${variableType}!`;
|
||||
return context.currentBlock?.title ?? '';
|
||||
case PromptVariableType.CONSTITUENTA:
|
||||
return context.currentConstituenta ? varConstituenta(context.currentConstituenta) : `!${variableType}!`;
|
||||
case PromptVariableType.CONSTITUENTA_SYNTAX_TREE:
|
||||
return context.currentConstituenta ? varSyntaxTree(context.currentConstituenta) : `!${variableType}!`;
|
||||
return context.currentConstituenta?.alias ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Internals =========
|
||||
function generateSchemaPrompt(schema: IRSForm): string {
|
||||
let body = `Название концептуальной схемы: ${schema.title}\n`;
|
||||
body += `[${schema.alias}] Описание: "${schema.description}"\n\n`;
|
||||
body += 'Понятия:\n';
|
||||
schema.items.forEach(item => {
|
||||
body += `${item.alias} - "${labelCstTypification(item)}" - "${item.term_resolved}" - "${
|
||||
item.definition_formal
|
||||
}" - "${item.definition_resolved}" - "${item.convention}"\n`;
|
||||
});
|
||||
return body;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user