Compare commits

..

42 Commits

Author SHA1 Message Date
Ivan
266fdf0c30 F: Implementing references pt2
Some checks failed
Backend CI / build (3.12) (push) Has been cancelled
Frontend CI / build (22.x) (push) Has been cancelled
Backend CI / notify-failure (push) Has been cancelled
Frontend CI / notify-failure (push) Has been cancelled
2025-08-04 22:56:29 +03:00
Ivan
13e56f51ea F: Implementing references pt1 2025-08-04 16:04:27 +03:00
Ivan
f78ee7c705 B: Fix OSS creation 2025-08-04 10:24:46 +03:00
Ivan
ce05b9de70 R: Refactoring caches pt3 2025-08-03 15:48:48 +03:00
Ivan
4794d36b6d R: Refactoring cache models pt1 2025-08-03 11:38:58 +03:00
Ivan
601ab8ce7b R: Segregate Cache models pt2 2025-08-01 18:33:30 +03:00
Ivan
687e646bf7 R: Segregate cached models pt1 2025-08-01 10:55:00 +03:00
Ivan
61b018f1bc F: Implementing operation reference pt1 2025-07-31 20:23:01 +03:00
Ivan
523185de9a Update oss.py 2025-07-30 21:55:42 +03:00
Ivan
361f870e15 x 2025-07-30 21:53:59 +03:00
Ivan
7c76becd85 x 2025-07-30 21:53:40 +03:00
Ivan
6b8608b53b F: Add clone-schema to OSS functions 2025-07-30 21:48:28 +03:00
Ivan
5bb2354d27 F: Improve Sidepanel UI 2025-07-30 10:36:37 +03:00
Ivan
18e016cbfb Update README.md 2025-07-29 23:21:20 +03:00
Ivan
fc13078dda F: Improve cst filters 2025-07-29 23:07:00 +03:00
Ivan
271a0399f0 x 2025-07-29 23:03:29 +03:00
Ivan
ceacd900c6 B: Small UI fixes 2025-07-29 22:41:57 +03:00
Ivan
eedeba0fc8 npm update 2025-07-29 21:25:13 +03:00
Ivan
18ad3f9f29 F: Implement crucial constituents UI 2025-07-29 20:56:56 +03:00
Ivan
e3b20551d5 F: Wire crucial attribute for frontend 2025-07-29 15:28:30 +03:00
Ivan
f78bda5057 B: Fix triple position color 2025-07-29 15:21:41 +03:00
Ivan
e69638799a F: Implement constituenta mark for backend 2025-07-29 13:45:53 +03:00
Ivan
97fe805e14 M: Select new items after creating them 2025-07-28 23:04:57 +03:00
Ivan
fc778827bc F: Implement hotkey navigation 2025-07-28 22:52:34 +03:00
Ivan
81e4d2ebd1 M: Minor UI improvements 2025-07-28 21:50:49 +03:00
Ivan
15b406f010 F: Improve graph filtering for CstList 2025-07-25 16:02:23 +03:00
Ivan
5a34116975 R: Refactor unnecessary tsx extensions 2025-07-25 13:21:55 +03:00
Ivan
1ef5330b89 F: Improve constituent relocation in OSS 2025-07-25 13:12:04 +03:00
Ivan
d2dc779130 F: Improve navigation tracking and loaders 2025-07-25 10:54:08 +03:00
Ivan
b1d2008c97 npm update: zod4 2025-07-24 12:32:18 +03:00
Ivan
ea6cedd4f4 M: Small UI fixes 2025-07-23 22:42:35 +03:00
Ivan
422e9139f4 F: Improve updating timestamps 2025-07-23 16:13:11 +03:00
Ivan
7a68e57a2b M: Improve positioning for new nodes 2025-07-23 15:28:25 +03:00
Ivan
cebe05dcb3 B: Fix rebuilding containers 2025-07-23 12:13:05 +03:00
Ivan
fd6ee0d736 F: Implement schema variables + small UI fixes 2025-07-23 12:09:01 +03:00
Ivan
ea892bc9cb F: Implement prompt editor 2025-07-22 20:38:08 +03:00
Ivan
78964b23cc F: Prompt editor component pt1 2025-07-22 14:52:03 +03:00
Ivan
20d95f42dc F: Improve Help documentation 2025-07-21 19:40:35 +03:00
Ivan
2484605375 M: Add restart to router after restarting containers 2025-07-21 11:35:19 +03:00
Ivan
ab82f1aaed F: Rework dialog for AI Prompts 2025-07-21 11:06:02 +03:00
Ivan
f27d9df8d3 F: Add JSON export option 2025-07-19 10:59:56 +03:00
Ivan
f7118bc96a npm update 2025-07-17 19:38:36 +03:00
287 changed files with 7515 additions and 4380 deletions

View File

@ -128,6 +128,7 @@
"perfectivity", "perfectivity",
"PNCT", "PNCT",
"ponomarev", "ponomarev",
"popleft",
"PRCL", "PRCL",
"PRTF", "PRTF",
"PRTS", "PRTS",

View File

@ -169,7 +169,7 @@ This readme file is used mostly to document project dependencies and conventions
This is the build for local Development This is the build for local Development
- Install Python 3.12, NodeJS, VSCode, Docker Desktop - Install Docker Desktop, Python 3.12, NodeJS, VSCode or other compatible IDE
- copy import wheels from ConceptCore to rsconcept/backend/import - copy import wheels from ConceptCore to rsconcept/backend/import
- run scripts/dev/LocalEnvSetup.ps1 - run scripts/dev/LocalEnvSetup.ps1
- use VSCode configs in root folder to start development - use VSCode configs in root folder to start development

View File

@ -1,10 +1,12 @@
''' Admin view: Library. ''' ''' Admin view: Library. '''
from typing import cast from typing import cast
from django.contrib import admin from django.contrib import admin
from . import models from . import models
@admin.register(models.LibraryItem)
class LibraryItemAdmin(admin.ModelAdmin): class LibraryItemAdmin(admin.ModelAdmin):
''' Admin model: LibraryItem. ''' ''' Admin model: LibraryItem. '''
date_hierarchy = 'time_update' date_hierarchy = 'time_update'
@ -17,6 +19,7 @@ class LibraryItemAdmin(admin.ModelAdmin):
search_fields = ['alias', 'title', 'location'] search_fields = ['alias', 'title', 'location']
@admin.register(models.LibraryTemplate)
class LibraryTemplateAdmin(admin.ModelAdmin): class LibraryTemplateAdmin(admin.ModelAdmin):
''' Admin model: LibraryTemplate. ''' ''' Admin model: LibraryTemplate. '''
list_display = ['id', 'alias'] list_display = ['id', 'alias']
@ -29,6 +32,7 @@ class LibraryTemplateAdmin(admin.ModelAdmin):
return 'N/A' return 'N/A'
@admin.register(models.Editor)
class EditorAdmin(admin.ModelAdmin): class EditorAdmin(admin.ModelAdmin):
''' Admin model: Editors. ''' ''' Admin model: Editors. '''
list_display = ['id', 'item', 'editor'] list_display = ['id', 'item', 'editor']
@ -38,16 +42,10 @@ class EditorAdmin(admin.ModelAdmin):
] ]
@admin.register(models.Version)
class VersionAdmin(admin.ModelAdmin): class VersionAdmin(admin.ModelAdmin):
''' Admin model: Versions. ''' ''' Admin model: Versions. '''
list_display = ['id', 'item', 'version', 'description', 'time_create'] list_display = ['id', 'item', 'version', 'description', 'time_create']
search_fields = [ search_fields = [
'item__title', 'item__alias' '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)

View File

@ -9,7 +9,6 @@ from apps.library.models import (
LibraryTemplate, LibraryTemplate,
LocationHead LocationHead
) )
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains from shared.testing_utils import response_contains
@ -59,8 +58,8 @@ class TestLibraryViewset(EndpointTester):
'read_only': True 'read_only': True
} }
response = self.executeCreated(data=data) response = self.executeCreated(data=data)
oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id'])) oss = LibraryItem.objects.get(pk=response.data['id'])
self.assertEqual(oss.model.owner, self.user) self.assertEqual(oss.owner, self.user)
self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], data['item_type']) self.assertEqual(response.data['item_type'], data['item_type'])
self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['title'], data['title'])
@ -334,12 +333,12 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/clone', method='post') @decl_endpoint('/api/library/{item}/clone', method='post')
def test_clone_rsform(self): def test_clone_rsform(self):
schema = RSForm(self.owned) schema = RSForm(self.owned)
x12 = schema.insert_new( x12 = schema.insert_last(
alias='X12', alias='X12',
term_raw='человек', term_raw='человек',
term_resolved='человек' term_resolved='человек'
) )
d2 = schema.insert_new( d2 = schema.insert_last(
alias='D2', alias='D2',
term_raw='@{X12|plur}', term_raw='@{X12|plur}',
term_resolved='люди' term_resolved='люди'

View File

@ -20,7 +20,7 @@ class TestVersionViews(EndpointTester):
self.owned_id = self.owned.model.pk self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2') self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk self.unowned_id = self.unowned.model.pk
self.x1 = self.owned.insert_new( self.x1 = self.owned.insert_last(
alias='X1', alias='X1',
convention='testStart' convention='testStart'
) )
@ -44,7 +44,7 @@ class TestVersionViews(EndpointTester):
@decl_endpoint('/api/library/{schema}/create-version', method='post') @decl_endpoint('/api/library/{schema}/create-version', method='post')
def test_create_version_filter(self): def test_create_version_filter(self):
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]} data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
response = self.executeCreated(data=data, schema=self.owned_id) response = self.executeCreated(data=data, schema=self.owned_id)
version = Version.objects.get(pk=response.data['version']) 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.executeNotFound(schema=self.unowned_id, version=version_id)
self.owned.model.alias = 'NewName' self.owned.model.alias = 'NewName'
self.owned.save() self.owned.model.save()
self.x1.alias = 'X33' self.x1.alias = 'X33'
self.x1.save() self.x1.save()
@ -84,15 +84,18 @@ class TestVersionViews(EndpointTester):
alias='A1', alias='A1',
cst_type='axiom', cst_type='axiom',
definition_formal='X1=X1', definition_formal='X1=X1',
order=1 order=1,
crucial=True
) )
version_id = self._create_version({'version': '1.0.0', 'description': 'test'}) version_id = self._create_version({'version': '1.0.0', 'description': 'test'})
a1.definition_formal = 'X1=X2' a1.definition_formal = 'X1=X2'
a1.crucial = False
a1.save() a1.save()
response = self.executeOK(schema=self.owned_id, version=version_id) response = self.executeOK(schema=self.owned_id, version=version_id)
loaded_a1 = response.data['items'][1] loaded_a1 = response.data['items'][1]
self.assertEqual(loaded_a1['definition_formal'], 'X1=X1') self.assertEqual(loaded_a1['definition_formal'], 'X1=X1')
self.assertEqual(loaded_a1['crucial'], True)
self.assertEqual(loaded_a1['parse']['status'], 'verified') self.assertEqual(loaded_a1['parse']['status'], 'verified')
@ -151,14 +154,14 @@ class TestVersionViews(EndpointTester):
@decl_endpoint('/api/versions/{version}/restore', method='patch') @decl_endpoint('/api/versions/{version}/restore', method='patch')
def test_restore_version(self): def test_restore_version(self):
x1 = self.x1 x1 = self.x1
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
d1 = self.owned.insert_new('D1', term_raw='TestTerm') d1 = self.owned.insert_last('D1', term_raw='TestTerm')
data = {'version': '1.0.0', 'description': 'test'} data = {'version': '1.0.0', 'description': 'test'}
version_id = self._create_version(data=data) version_id = self._create_version(data=data)
invalid_id = version_id + 1337 invalid_id = version_id + 1337
self.owned.delete_cst([d1]) Constituenta.objects.get(pk=d1.pk).delete()
x3 = self.owned.insert_new('X3') x3 = self.owned.insert_last('X3')
x1.order = x3.order x1.order = x3.order
x1.convention = 'Test2' x1.convention = 'Test2'
x1.term_raw = 'Test' x1.term_raw = 'Test'

View File

@ -14,7 +14,7 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
from apps.rsform.models import RSForm from apps.rsform.models import RSFormCached
from apps.rsform.serializers import RSFormParseSerializer from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User from apps.users.models import User
from shared import permissions from shared import permissions
@ -70,7 +70,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
PropagationFacade.before_delete_schema(instance) PropagationFacade.before_delete_schema(instance)
super().perform_destroy(instance) super().perform_destroy(instance)
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA: if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
schemas = list(OperationSchema(instance).owned_schemas()) schemas = list(OperationSchema.owned_schemasQ(instance))
super().perform_destroy(instance) super().perform_destroy(instance)
for schema in schemas: for schema in schemas:
self.perform_destroy(schema) self.perform_destroy(schema)
@ -159,6 +159,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
with transaction.atomic():
clone = deepcopy(item) clone = deepcopy(item)
clone.pk = None clone.pk = None
clone.owner = cast(User, self.request.user) clone.owner = cast(User, self.request.user)
@ -169,11 +170,9 @@ class LibraryViewSet(viewsets.ModelViewSet):
clone.read_only = False clone.read_only = False
clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC) clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
clone.location = data.get('location', m.LocationHead.USER) clone.location = data.get('location', m.LocationHead.USER)
with transaction.atomic():
clone.save() clone.save()
need_filter = 'items' in request.data and len(request.data['items']) > 0 need_filter = 'items' in request.data and len(request.data['items']) > 0
for cst in RSForm(item).constituents(): for cst in RSFormCached(item).constituentsQ():
if not need_filter or cst.pk in request.data['items']: if not need_filter or cst.pk in request.data['items']:
cst.pk = None cst.pk = None
cst.schema = clone cst.schema = clone
@ -205,7 +204,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('owner') owned_schemas = OperationSchema.owned_schemasQ(item).only('owner')
for schema in owned_schemas: for schema in owned_schemas:
schema.owner_id = new_owner schema.owner_id = new_owner
m.LibraryItem.objects.bulk_update(owned_schemas, ['owner']) m.LibraryItem.objects.bulk_update(owned_schemas, ['owner'])
@ -239,7 +238,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('location') owned_schemas = OperationSchema.owned_schemasQ(item).only('location')
for schema in owned_schemas: for schema in owned_schemas:
schema.location = location schema.location = location
m.LibraryItem.objects.bulk_update(owned_schemas, ['location']) m.LibraryItem.objects.bulk_update(owned_schemas, ['location'])
@ -271,7 +270,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA: if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('access_policy') owned_schemas = OperationSchema.owned_schemasQ(item).only('access_policy')
for schema in owned_schemas: for schema in owned_schemas:
schema.access_policy = new_policy schema.access_policy = new_policy
m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy']) m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy'])
@ -301,7 +300,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic(): with transaction.atomic():
added, deleted = m.Editor.set_and_return_diff(item.pk, editors) added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
if len(added) >= 0 or len(deleted) >= 0: if len(added) >= 0 or len(deleted) >= 0:
owned_schemas = OperationSchema(item).owned_schemas().only('pk') owned_schemas = OperationSchema.owned_schemasQ(item).only('pk')
if owned_schemas.exists(): if owned_schemas.exists():
m.Editor.objects.filter( m.Editor.objects.filter(
item__in=owned_schemas, item__in=owned_schemas,

View File

@ -47,6 +47,7 @@ class VersionViewset(
item = version.item item = version.item
with transaction.atomic(): with transaction.atomic():
RSFormSerializer(item).restore_from_version(version.data) RSFormSerializer(item).restore_from_version(version.data)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=RSFormParseSerializer(item).data data=RSFormParseSerializer(item).data
@ -69,7 +70,7 @@ def export_file(request: Request, pk: int) -> HttpResponse:
version = m.Version.objects.get(pk=pk) version = m.Version.objects.get(pk=pk)
except m.Version.DoesNotExist: except m.Version.DoesNotExist:
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_404_NOT_FOUND)
data = RSFormTRSSerializer(version.item).from_versioned_data(version.data) data = RSFormTRSSerializer.load_versioned_data(version.data)
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(data['alias']) filename = utils.filename_for_schema(data['alias'])
response = HttpResponse(file, content_type='application/zip') response = HttpResponse(file, content_type='application/zip')

View File

@ -4,6 +4,7 @@ from django.contrib import admin
from . import models from . import models
@admin.register(models.Operation)
class OperationAdmin(admin.ModelAdmin): class OperationAdmin(admin.ModelAdmin):
''' Admin model: Operation. ''' ''' Admin model: Operation. '''
ordering = ['oss'] ordering = ['oss']
@ -19,6 +20,7 @@ class OperationAdmin(admin.ModelAdmin):
search_fields = ['id', 'operation_type', 'title', 'alias'] search_fields = ['id', 'operation_type', 'title', 'alias']
@admin.register(models.Block)
class BlockAdmin(admin.ModelAdmin): class BlockAdmin(admin.ModelAdmin):
''' Admin model: Block. ''' ''' Admin model: Block. '''
ordering = ['oss'] ordering = ['oss']
@ -26,6 +28,7 @@ class BlockAdmin(admin.ModelAdmin):
search_fields = ['oss'] search_fields = ['oss']
@admin.register(models.Layout)
class LayoutAdmin(admin.ModelAdmin): class LayoutAdmin(admin.ModelAdmin):
''' Admin model: Layout. ''' ''' Admin model: Layout. '''
ordering = ['oss'] ordering = ['oss']
@ -33,6 +36,7 @@ class LayoutAdmin(admin.ModelAdmin):
search_fields = ['oss'] search_fields = ['oss']
@admin.register(models.Argument)
class ArgumentAdmin(admin.ModelAdmin): class ArgumentAdmin(admin.ModelAdmin):
''' Admin model: Operation arguments. ''' ''' Admin model: Operation arguments. '''
ordering = ['operation'] ordering = ['operation']
@ -40,6 +44,7 @@ class ArgumentAdmin(admin.ModelAdmin):
search_fields = ['id', 'operation', 'argument'] search_fields = ['id', 'operation', 'argument']
@admin.register(models.Substitution)
class SynthesisSubstitutionAdmin(admin.ModelAdmin): class SynthesisSubstitutionAdmin(admin.ModelAdmin):
''' Admin model: Substitutions as part of Synthesis operation. ''' ''' Admin model: Substitutions as part of Synthesis operation. '''
ordering = ['operation'] ordering = ['operation']
@ -47,6 +52,7 @@ class SynthesisSubstitutionAdmin(admin.ModelAdmin):
search_fields = ['id', 'operation', 'original', 'substitution'] search_fields = ['id', 'operation', 'original', 'substitution']
@admin.register(models.Inheritance)
class InheritanceAdmin(admin.ModelAdmin): class InheritanceAdmin(admin.ModelAdmin):
''' Admin model: Inheritance. ''' ''' Admin model: Inheritance. '''
ordering = ['operation'] ordering = ['operation']
@ -54,9 +60,9 @@ class InheritanceAdmin(admin.ModelAdmin):
search_fields = ['id', 'operation', 'parent', 'child'] search_fields = ['id', 'operation', 'parent', 'child']
admin.site.register(models.Operation, OperationAdmin) @admin.register(models.Reference)
admin.site.register(models.Block, BlockAdmin) class ReferenceAdmin(admin.ModelAdmin):
admin.site.register(models.Layout, LayoutAdmin) ''' Admin model: Reference. '''
admin.site.register(models.Argument, ArgumentAdmin) ordering = ['reference', 'target']
admin.site.register(models.Substitution, SynthesisSubstitutionAdmin) list_display = ['id', 'reference', 'target']
admin.site.register(models.Inheritance, InheritanceAdmin) search_fields = ['id', 'reference', 'target']

View File

@ -0,0 +1,18 @@
# 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='Тип'),
),
]

View File

@ -0,0 +1,27 @@
# 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')},
},
),
]

View File

@ -23,3 +23,10 @@ class Layout(Model):
def __str__(self) -> str: def __str__(self) -> str:
return f'Схема расположения {self.oss.alias}' 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()

View File

@ -1,5 +1,7 @@
''' Models: Operation in OSS. ''' ''' Models: Operation in OSS. '''
# pylint: disable=duplicate-code # pylint: disable=duplicate-code
from typing import Optional
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
SET_NULL, SET_NULL,
@ -11,7 +13,10 @@ from django.db.models import (
TextField TextField
) )
from apps.library.models import LibraryItem
from .Argument import Argument from .Argument import Argument
from .Reference import Reference
from .Substitution import Substitution from .Substitution import Substitution
@ -19,6 +24,7 @@ class OperationType(TextChoices):
''' Type of operation. ''' ''' Type of operation. '''
INPUT = 'input' INPUT = 'input'
SYNTHESIS = 'synthesis' SYNTHESIS = 'synthesis'
REFERENCE = 'reference'
class Operation(Model): class Operation(Model):
@ -76,9 +82,37 @@ class Operation(Model):
return f'Операция {self.alias}' return f'Операция {self.alias}'
def getQ_arguments(self) -> QuerySet[Argument]: def getQ_arguments(self) -> QuerySet[Argument]:
''' Operation arguments. ''' ''' Operation Arguments for current operation. '''
return Argument.objects.filter(operation=self) 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]: def getQ_substitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. ''' ''' Operation substitutions. '''
return Substitution.objects.filter(operation=self) 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)

View File

@ -1,40 +1,25 @@
''' Models: OSS API. ''' ''' Models: OSS API. '''
from typing import Optional, cast # pylint: disable=duplicate-code
from cctext import extract_entities
from django.db.models import QuerySet from django.db.models import QuerySet
from rest_framework.serializers import ValidationError
from apps.library.models import Editor, LibraryItem, LibraryItemType from apps.library.models import Editor, LibraryItem, LibraryItemType
from apps.rsform.graph import Graph from apps.rsform.models import Constituenta, OrderManager, RSFormCached
from apps.rsform.models import (
DELETED_ALIAS,
INSERT_LAST,
Constituenta,
CstType,
RSForm,
extract_globals,
replace_entities,
replace_globals
)
from .Argument import Argument from .Argument import Argument
from .Block import Block from .Block import Block
from .Inheritance import Inheritance from .Inheritance import Inheritance
from .Layout import Layout from .Layout import Layout
from .Operation import Operation from .Operation import Operation, OperationType
from .Reference import Reference
from .Substitution import Substitution from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class OperationSchema: class OperationSchema:
''' Operations schema API. ''' ''' Operations schema API wrapper. No caching, propagation and minimal side effects. '''
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem):
self.model = model self.model = model
self.cache = OssCache(self)
@staticmethod @staticmethod
def create(**kwargs) -> 'OperationSchema': def create(**kwargs) -> 'OperationSchema':
@ -44,101 +29,44 @@ class OperationSchema:
return OperationSchema(model) return OperationSchema(model)
@staticmethod @staticmethod
def from_id(pk: int) -> 'OperationSchema': def owned_schemasQ(item: LibraryItem) -> QuerySet[LibraryItem]:
''' Get LibraryItem by pk. ''' ''' Get QuerySet containing all result schemas owned by current OSS. '''
model = LibraryItem.objects.get(pk=pk) return LibraryItem.objects.filter(
return OperationSchema(model) producer__oss=item,
owner_id=item.owner_id,
location=item.location
)
def save(self, *args, **kwargs) -> None: @staticmethod
''' Save wrapper. ''' def layoutQ(itemID: int) -> Layout:
self.model.save(*args, **kwargs) ''' OSS layout. '''
return Layout.objects.get(oss_id=itemID)
def refresh_from_db(self) -> None: def refresh_from_db(self) -> None:
''' Model wrapper. ''' ''' Model wrapper. '''
self.model.refresh_from_db() 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: def create_operation(self, **kwargs) -> Operation:
''' Create Operation. ''' ''' Create Operation. '''
result = Operation.objects.create(oss=self.model, **kwargs) result = Operation.objects.create(oss=self.model, **kwargs)
self.cache.insert_operation(result) return result
self.save(update_fields=['time_update'])
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)
return result return result
def create_block(self, **kwargs) -> Block: def create_block(self, **kwargs) -> Block:
''' Create Block. ''' ''' Create Block. '''
result = Block.objects.create(oss=self.model, **kwargs) result = Block.objects.create(oss=self.model, **kwargs)
self.save(update_fields=['time_update'])
return result 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): def delete_block(self, target: Block):
''' Delete Block. ''' ''' Delete Block. '''
new_parent = target.parent new_parent = target.parent
@ -151,108 +79,10 @@ class OperationSchema:
operation.parent = new_parent operation.parent = new_parent
operation.save(update_fields=['parent']) operation.save(update_fields=['parent'])
target.delete() target.delete()
self.save(update_fields=['time_update'])
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None: def create_input(self, operation: Operation) -> RSFormCached:
''' 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. ''' ''' Create input RSForm for given Operation. '''
schema = RSForm.create( schema = RSFormCached.create(
owner=self.model.owner, owner=self.model.owner,
alias=operation.alias, alias=operation.alias,
title=operation.title, title=operation.title,
@ -262,28 +92,51 @@ class OperationSchema:
location=self.model.location location=self.model.location
) )
Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True)) Editor.set(schema.model.pk, self.model.getQ_editors().values_list('pk', flat=True))
operation.result = schema.model operation.setQ_result(schema.model)
operation.save()
self.save(update_fields=['time_update'])
return schema return schema
def execute_operation(self, operation: Operation) -> bool: 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:
''' Execute target Operation. ''' ''' Execute target Operation. '''
schemas = [ schemas: list[int] = [
arg.argument.result arg.argument.result_id
for arg in operation.getQ_arguments().order_by('order') for arg in Argument.objects
if arg.argument.result is not None .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: if len(schemas) == 0:
return False return
substitutions = operation.getQ_substitutions() substitutions = operation.getQ_substitutions()
receiver = self.create_input(self.cache.operation_by_id[operation.pk]) receiver = self.create_input(operation)
parents: dict = {} parents: dict = {}
children: dict = {} children: dict = {}
for operand in schemas: for operand in schemas:
schema = RSForm(operand) items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
items = list(schema.constituents().order_by('order'))
new_items = receiver.insert_copy(items) new_items = receiver.insert_copy(items)
for (i, cst) in enumerate(new_items): for (i, cst) in enumerate(new_items):
parents[cst.pk] = items[i] parents[cst.pk] = items[i]
@ -296,7 +149,7 @@ class OperationSchema:
translated_substitutions.append((original, replacement)) translated_substitutions.append((original, replacement))
receiver.substitute(translated_substitutions) receiver.substitute(translated_substitutions)
for cst in receiver.constituents().order_by('order'): for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
parent = parents.get(cst.pk) parent = parents.get(cst.pk)
assert parent is not None assert parent is not None
Inheritance.objects.create( Inheritance.objects.create(
@ -305,646 +158,6 @@ class OperationSchema:
parent=parent parent=parent
) )
receiver.restore_order() OrderManager(receiver).restore_order()
receiver.reset_aliases() receiver.reset_aliases()
receiver.resolve_all_text() 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

View File

@ -0,0 +1,881 @@
''' 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

View File

@ -2,73 +2,79 @@
from typing import Optional from typing import Optional
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, CstType, RSFormCached
from .OperationSchema import CstSubstitution, OperationSchema from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
def _get_oss_hosts(item: LibraryItem) -> list[LibraryItem]: def _get_oss_hosts(schemaID: int) -> list[LibraryItem]:
''' Get all hosts for LibraryItem. ''' ''' Get all hosts for LibraryItem. '''
return list(LibraryItem.objects.filter(operations__result=item).only('pk')) return list(LibraryItem.objects.filter(operations__result_id=schemaID).only('pk').distinct())
class PropagationFacade: class PropagationFacade:
''' Change propagation API. ''' ''' Change propagation API. '''
@staticmethod @staticmethod
def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None: def after_create_cst(source: RSFormCached, new_cst: list[Constituenta],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when new constituenta is created. ''' ''' Trigger cascade resolutions when new constituenta is created. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model.pk)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host.pk not in exclude:
OperationSchema(host).after_create_cst(source, new_cst) OperationSchemaCached(host).after_create_cst(source, new_cst)
@staticmethod @staticmethod
def after_change_cst_type(source: RSForm, target: Constituenta, exclude: Optional[list[int]] = None) -> None: def after_change_cst_type(sourceID: int, target: int, new_type: CstType,
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when constituenta type is changed. ''' ''' Trigger cascade resolutions when constituenta type is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host.pk not in exclude:
OperationSchema(host).after_change_cst_type(source, target) OperationSchemaCached(host).after_change_cst_type(sourceID, target, new_type)
@staticmethod @staticmethod
def after_update_cst( def after_update_cst(
source: RSForm, source: RSFormCached,
target: Constituenta, target: int,
data: dict, data: dict,
old_data: dict, old_data: dict,
exclude: Optional[list[int]] = None exclude: Optional[list[int]] = None
) -> None: ) -> None:
''' Trigger cascade resolutions when constituenta data is changed. ''' ''' Trigger cascade resolutions when constituenta data is changed. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(source.model.pk)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host.pk not in exclude:
OperationSchema(host).after_update_cst(source, target, data, old_data) OperationSchemaCached(host).after_update_cst(source, target, data, old_data)
@staticmethod @staticmethod
def before_delete_cst(source: RSForm, target: list[Constituenta], exclude: Optional[list[int]] = None) -> None: def before_delete_cst(sourceID: int, target: list[int],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before constituents are deleted. ''' ''' Trigger cascade resolutions before constituents are deleted. '''
hosts = _get_oss_hosts(source.model) hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host.pk not in exclude:
OperationSchema(host).before_delete_cst(source, target) OperationSchemaCached(host).before_delete_cst(sourceID, target)
@staticmethod @staticmethod
def before_substitute(source: RSForm, substitutions: CstSubstitution, exclude: Optional[list[int]] = None) -> None: def before_substitute(sourceID: int, substitutions: CstSubstitution,
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before constituents are substituted. ''' ''' Trigger cascade resolutions before constituents are substituted. '''
hosts = _get_oss_hosts(source.model) if len(substitutions) == 0:
return
hosts = _get_oss_hosts(sourceID)
for host in hosts: for host in hosts:
if exclude is None or host.pk not in exclude: if exclude is None or host.pk not in exclude:
OperationSchema(host).before_substitute(source, substitutions) OperationSchemaCached(host).before_substitute(sourceID, substitutions)
@staticmethod @staticmethod
def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None: def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before schema is deleted. ''' ''' Trigger cascade resolutions before schema is deleted. '''
if item.item_type != LibraryItemType.RSFORM: if item.item_type != LibraryItemType.RSFORM:
return return
hosts = _get_oss_hosts(item) hosts = _get_oss_hosts(item.pk)
if len(hosts) == 0: if len(hosts) == 0:
return return
schema = RSForm(item) ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order')), exclude) PropagationFacade.before_delete_cst(item.pk, ids, exclude)

View File

@ -0,0 +1,27 @@
''' 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}'

View File

@ -6,5 +6,7 @@ from .Inheritance import Inheritance
from .Layout import Layout from .Layout import Layout
from .Operation import Operation, OperationType from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema from .OperationSchema import OperationSchema
from .OperationSchemaCached import OperationSchemaCached
from .PropagationFacade import PropagationFacade from .PropagationFacade import PropagationFacade
from .Reference import Reference
from .Substitution import Substitution from .Substitution import Substitution

View File

@ -4,15 +4,19 @@ from .basics import LayoutSerializer, SubstitutionExSerializer
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
BlockSerializer, BlockSerializer,
CloneSchemaSerializer,
CreateBlockSerializer, CreateBlockSerializer,
CreateReferenceSerializer,
CreateSchemaSerializer, CreateSchemaSerializer,
CreateSynthesisSerializer, CreateSynthesisSerializer,
DeleteBlockSerializer, DeleteBlockSerializer,
DeleteOperationSerializer, DeleteOperationSerializer,
DeleteReferenceSerializer,
ImportSchemaSerializer, ImportSchemaSerializer,
MoveItemsSerializer, MoveItemsSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,
ReferenceSerializer,
RelocateConstituentsSerializer, RelocateConstituentsSerializer,
SetOperationInputSerializer, SetOperationInputSerializer,
TargetOperationSerializer, TargetOperationSerializer,

View File

@ -13,7 +13,16 @@ from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType from ..models import (
Argument,
Block,
Inheritance,
Layout,
Operation,
OperationType,
Reference,
Substitution
)
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
@ -45,6 +54,14 @@ class ArgumentSerializer(StrictModelSerializer):
fields = ('operation', 'argument') fields = ('operation', 'argument')
class ReferenceSerializer(StrictModelSerializer):
''' Serializer: Reference data. '''
class Meta:
''' serializer metadata. '''
model = Reference
fields = ('reference', 'target')
class CreateBlockSerializer(StrictSerializer): class CreateBlockSerializer(StrictSerializer):
''' Serializer: Block creation. ''' ''' Serializer: Block creation. '''
class BlockCreateData(StrictModelSerializer): class BlockCreateData(StrictModelSerializer):
@ -217,6 +234,50 @@ class CreateSchemaSerializer(StrictSerializer):
return attrs 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): class ImportSchemaSerializer(StrictSerializer):
''' Serializer: Import schema to new operation. ''' ''' Serializer: Import schema to new operation. '''
layout = serializers.ListField(child=NodeSerializer()) layout = serializers.ListField(child=NodeSerializer())
@ -247,7 +308,7 @@ class CreateSynthesisSerializer(StrictSerializer):
arguments = PKField( arguments = PKField(
many=True, many=True,
queryset=Operation.objects.all().only('pk') queryset=Operation.objects.all().only('pk', 'result_id')
) )
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionSerializerBase(), child=SubstitutionSerializerBase(),
@ -371,11 +432,11 @@ class UpdateOperationSerializer(StrictSerializer):
class DeleteOperationSerializer(StrictSerializer): class DeleteOperationSerializer(StrictSerializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete non-reference operation. '''
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
) )
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result')) target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type', 'result'))
keep_constituents = serializers.BooleanField(default=False, required=False) keep_constituents = serializers.BooleanField(default=False, required=False)
delete_schema = serializers.BooleanField(default=False, required=False) delete_schema = serializers.BooleanField(default=False, required=False)
@ -386,6 +447,33 @@ class DeleteOperationSerializer(StrictSerializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationNotInOSS() '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 return attrs
@ -447,6 +535,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=SubstitutionExSerializer() child=SubstitutionExSerializer()
) )
references = serializers.ListField(
child=ReferenceSerializer()
)
layout = serializers.ListField( layout = serializers.ListField(
child=NodeSerializer() child=NodeSerializer()
) )
@ -459,13 +550,13 @@ class OperationSchemaSerializer(StrictModelSerializer):
def to_representation(self, instance: LibraryItem): def to_representation(self, instance: LibraryItem):
result = LibraryItemDetailsSerializer(instance).data result = LibraryItemDetailsSerializer(instance).data
del result['versions'] del result['versions']
oss = OperationSchema(instance) result['layout'] = Layout.objects.get(oss=instance).data
result['layout'] = oss.layout().data
result['operations'] = [] result['operations'] = []
result['blocks'] = [] result['blocks'] = []
result['arguments'] = [] result['arguments'] = []
result['substitutions'] = [] result['substitutions'] = []
for operation in oss.operations().order_by('pk'): result['references'] = []
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
operation_data = OperationSerializer(operation).data operation_data = OperationSerializer(operation).data
operation_result = operation.result operation_result = operation.result
operation_data['is_import'] = \ operation_data['is_import'] = \
@ -473,11 +564,11 @@ class OperationSchemaSerializer(StrictModelSerializer):
(operation_result.owner_id != instance.owner_id or (operation_result.owner_id != instance.owner_id or
operation_result.location != instance.location) operation_result.location != instance.location)
result['operations'].append(operation_data) result['operations'].append(operation_data)
for block in oss.blocks().order_by('pk'): for block in Block.objects.filter(oss=instance).order_by('pk'):
result['blocks'].append(BlockSerializer(block).data) result['blocks'].append(BlockSerializer(block).data)
for argument in oss.arguments().order_by('order'): for argument in Argument.objects.filter(operation__oss=instance).order_by('order'):
result['arguments'].append(ArgumentSerializer(argument).data) result['arguments'].append(ArgumentSerializer(argument).data)
for substitution in oss.substitutions().values( for substitution in Substitution.objects.filter(operation__oss=instance).values(
'operation', 'operation',
'original', 'original',
'substitution', 'substitution',
@ -487,6 +578,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitution_term=F('substitution__term_resolved'), substitution_term=F('substitution__term_resolved'),
).order_by('pk'): ).order_by('pk'):
result['substitutions'].append(substitution) result['substitutions'].append(substitution)
for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
result['references'].append(ReferenceSerializer(reference).data)
return result return result

View File

@ -1,5 +1,7 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Argument import * from .t_Argument import *
from .t_Inheritance import * from .t_Inheritance import *
from .t_Layout import *
from .t_Operation import * from .t_Operation import *
from .t_Reference import *
from .t_Substitution import * from .t_Substitution import *

View File

@ -20,8 +20,8 @@ class TestInheritance(TestCase):
operation_type=OperationType.INPUT, operation_type=OperationType.INPUT,
result=self.ks1.model result=self.ks1.model
) )
self.ks1_x1 = self.ks1.insert_new('X1') self.ks1_x1 = self.ks1.insert_last('X1')
self.ks2_x1 = self.ks2.insert_new('X1') self.ks2_x1 = self.ks2.insert_last('X1')
self.inheritance = Inheritance.objects.create( self.inheritance = Inheritance.objects.create(
operation=self.operation, operation=self.operation,
parent=self.ks1_x1, parent=self.ks1_x1,

View File

@ -0,0 +1,39 @@
''' 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)

View File

@ -3,7 +3,6 @@ from django.test import TestCase
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm
class TestOperation(TestCase): class TestOperation(TestCase):

View File

@ -0,0 +1,44 @@
''' 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)

View File

@ -15,9 +15,9 @@ class TestSynthesisSubstitution(TestCase):
self.oss = OperationSchema.create(alias='T1') self.oss = OperationSchema.create(alias='T1')
self.ks1 = RSForm.create(alias='KS1', title='Test1') self.ks1 = RSForm.create(alias='KS1', title='Test1')
self.ks1X1 = self.ks1.insert_new('X1', term_resolved='X1_1') self.ks1X1 = self.ks1.insert_last('X1', term_resolved='X1_1')
self.ks2 = RSForm.create(alias='KS2', title='Test2') self.ks2 = RSForm.create(alias='KS2', title='Test2')
self.ks2X1 = self.ks2.insert_new('X2', term_resolved='X1_2') self.ks2X1 = self.ks2.insert_last('X2', term_resolved='X1_2')
self.operation1 = Operation.objects.create( self.operation1 = Operation.objects.create(
oss=self.oss.model, oss=self.oss.model,

View File

@ -2,4 +2,5 @@
from .t_attributes import * from .t_attributes import *
from .t_constituents import * from .t_constituents import *
from .t_operations import * from .t_operations import *
from .t_references import *
from .t_substitutions import * from .t_substitutions import *

View File

@ -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.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.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
@ -75,10 +75,10 @@ class TestChangeAttributes(EndpointTester):
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks2.refresh_from_db() self.ks2.model.refresh_from_db()
self.ks3.refresh_from_db() self.ks3.model.refresh_from_db()
self.assertEqual(self.owned.model.owner, self.user3) self.assertEqual(self.owned.model.owner, self.user3)
self.assertEqual(self.ks1.model.owner, self.user) self.assertEqual(self.ks1.model.owner, self.user)
self.assertEqual(self.ks2.model.owner, self.user2) self.assertEqual(self.ks2.model.owner, self.user2)
@ -91,10 +91,10 @@ class TestChangeAttributes(EndpointTester):
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks2.refresh_from_db() self.ks2.model.refresh_from_db()
self.ks3.refresh_from_db() self.ks3.model.refresh_from_db()
self.assertEqual(self.owned.model.location, data['location']) self.assertEqual(self.owned.model.location, data['location'])
self.assertNotEqual(self.ks1.model.location, data['location']) self.assertNotEqual(self.ks1.model.location, data['location'])
self.assertNotEqual(self.ks2.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.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks2.refresh_from_db() self.ks2.model.refresh_from_db()
self.ks3.refresh_from_db() self.ks3.model.refresh_from_db()
self.assertEqual(self.owned.model.access_policy, data['access_policy']) self.assertEqual(self.owned.model.access_policy, data['access_policy'])
self.assertNotEqual(self.ks1.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']) 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.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks2.refresh_from_db() self.ks2.model.refresh_from_db()
self.ks3.refresh_from_db() self.ks3.model.refresh_from_db()
self.assertEqual(list(self.owned.model.getQ_editors()), [self.user3]) 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.ks1.model.getQ_editors()), [self.user, self.user2])
self.assertEqual(list(self.ks2.model.getQ_editors()), []) self.assertEqual(list(self.ks2.model.getQ_editors()), [])
@ -162,7 +162,7 @@ class TestChangeAttributes(EndpointTester):
} }
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
self.ks3.refresh_from_db() self.ks3.model.refresh_from_db()
self.assertEqual(self.ks3.model.alias, data['item_data']['alias']) 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.title, data['item_data']['title'])
self.assertEqual(self.ks3.model.description, data['item_data']['description']) self.assertEqual(self.ks3.model.description, data['item_data']['description'])

View File

@ -22,16 +22,16 @@ class TestChangeConstituents(EndpointTester):
title='Test1', title='Test1',
owner=self.user owner=self.user
) )
self.ks1X1 = self.ks1.insert_new('X4') self.ks1X1 = self.ks1.insert_last('X4')
self.ks1X2 = self.ks1.insert_new('X5') self.ks1X2 = self.ks1.insert_last('X5')
self.ks2 = RSForm.create( self.ks2 = RSForm.create(
alias='KS2', alias='KS2',
title='Test2', title='Test2',
owner=self.user owner=self.user
) )
self.ks2X1 = self.ks2.insert_new('X1') self.ks2X1 = self.ks2.insert_last('X1')
self.ks2D1 = self.ks2.insert_new( self.ks2D1 = self.ks2.insert_last(
alias='D1', alias='D1',
definition_formal=r'X1\X1' definition_formal=r'X1\X1'
) )
@ -55,14 +55,14 @@ class TestChangeConstituents(EndpointTester):
self.owned.execute_operation(self.operation3) self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
self.ks3 = RSForm(self.operation3.result) self.ks3 = RSForm(self.operation3.result)
self.assertEqual(self.ks3.constituents().count(), 4) self.assertEqual(self.ks3.constituentsQ().count(), 4)
self.layout_data = [ self.layout_data = [
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'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.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.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
@ -105,8 +105,8 @@ class TestChangeConstituents(EndpointTester):
response = self.executeCreated(data=data, schema=self.ks1.model.pk) response = self.executeCreated(data=data, schema=self.ks1.model.pk)
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id']) new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk) inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituentsQ().count(), 3)
self.assertEqual(self.ks3.constituents().count(), 5) self.assertEqual(self.ks3.constituentsQ().count(), 5)
self.assertEqual(inherited_cst.alias, 'X4') self.assertEqual(inherited_cst.alias, 'X4')
self.assertEqual(inherited_cst.order, 2) self.assertEqual(inherited_cst.order, 2)
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2') self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
@ -114,14 +114,15 @@ class TestChangeConstituents(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch') @decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_constituenta(self): def test_update_constituenta(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}') d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
data = { data = {
'target': self.ks1X1.pk, 'target': self.ks1X1.pk,
'item_data': { 'item_data': {
'term_raw': 'Test1', 'term_raw': 'Test1',
'definition_formal': r'X4\X4', 'definition_formal': r'X4\X4',
'definition_raw': '@{X5|sing,datv}', 'definition_raw': '@{X5|sing,datv}',
'convention': 'test' 'convention': 'test',
'crucial': True,
} }
} }
response = self.executeOK(data=data, schema=self.ks1.model.pk) response = self.executeOK(data=data, schema=self.ks1.model.pk)
@ -132,9 +133,11 @@ class TestChangeConstituents(EndpointTester):
self.assertEqual(self.ks1X1.definition_formal, data['item_data']['definition_formal']) 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.definition_raw, data['item_data']['definition_raw'])
self.assertEqual(self.ks1X1.convention, data['item_data']['convention']) 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(d2.definition_resolved, data['item_data']['term_raw'])
self.assertEqual(inherited_cst.term_raw, 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.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_formal, r'X1\X1')
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}') self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
@ -145,15 +148,15 @@ class TestChangeConstituents(EndpointTester):
response = self.executeOK(data=data, schema=self.ks2.model.pk) response = self.executeOK(data=data, schema=self.ks2.model.pk)
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk) inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
self.ks2D1.refresh_from_db() self.ks2D1.refresh_from_db()
self.assertEqual(self.ks2.constituents().count(), 1) self.assertEqual(self.ks2.constituentsQ().count(), 1)
self.assertEqual(self.ks3.constituents().count(), 3) self.assertEqual(self.ks3.constituentsQ().count(), 3)
self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL') self.assertEqual(self.ks2D1.definition_formal, r'DEL\DEL')
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL') self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch') @decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
def test_substitute(self): def test_substitute(self):
d2 = self.ks3.insert_new('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3') d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
data = {'substitutions': [{ data = {'substitutions': [{
'original': self.ks1X1.pk, 'original': self.ks1X1.pk,
'substitution': self.ks1X2.pk 'substitution': self.ks1X2.pk
@ -161,7 +164,7 @@ class TestChangeConstituents(EndpointTester):
self.executeOK(data=data, schema=self.ks1.model.pk) self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X2.refresh_from_db() self.ks1X2.refresh_from_db()
d2.refresh_from_db() d2.refresh_from_db()
self.assertEqual(self.ks1.constituents().count(), 1) self.assertEqual(self.ks1.constituentsQ().count(), 1)
self.assertEqual(self.ks3.constituents().count(), 4) self.assertEqual(self.ks3.constituentsQ().count(), 4)
self.assertEqual(self.ks1X2.order, 0) self.assertEqual(self.ks1X2.order, 0)
self.assertEqual(d2.definition_formal, r'X2\X2\X3') self.assertEqual(d2.definition_formal, r'X2\X2\X3')

View File

@ -8,7 +8,6 @@ from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeOperations(EndpointTester): class TestChangeOperations(EndpointTester):
''' Testing Operations change propagation in OSS. ''' ''' Testing Operations change propagation in OSS. '''
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = OperationSchema.create( self.owned = OperationSchema.create(
@ -23,18 +22,18 @@ class TestChangeOperations(EndpointTester):
title='Test1', title='Test1',
owner=self.user owner=self.user
) )
self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1') self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2') self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1') self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
self.ks2 = RSForm.create( self.ks2 = RSForm.create(
alias='KS2', alias='KS2',
title='Test2', title='Test2',
owner=self.user owner=self.user
) )
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1') self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2') self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
self.ks2S1 = self.ks2.insert_new( self.ks2S1 = self.ks2.insert_last(
alias='S1', alias='S1',
definition_formal=r'X1', definition_formal=r'X1',
convention='KS2S1' convention='KS2S1'
@ -45,8 +44,8 @@ class TestChangeOperations(EndpointTester):
title='Test3', title='Test3',
owner=self.user owner=self.user
) )
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1') self.ks3X1 = self.ks3.insert_last('X1', convention='KS3X1')
self.ks3D1 = self.ks3.insert_new( self.ks3D1 = self.ks3.insert_last(
alias='D1', alias='D1',
definition_formal='X1 X1', definition_formal='X1 X1',
convention='KS3D1' convention='KS3D1'
@ -83,7 +82,7 @@ class TestChangeOperations(EndpointTester):
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) 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.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk) self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
self.ks4D2 = self.ks4.insert_new( self.ks4D2 = self.ks4.insert_last(
alias='D2', alias='D2',
definition_formal=r'X1 X2 X3 S1 D1', definition_formal=r'X1 X2 X3 S1 D1',
convention='KS4D2' convention='KS4D2'
@ -101,7 +100,7 @@ class TestChangeOperations(EndpointTester):
self.owned.execute_operation(self.operation5) self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db() self.operation5.refresh_from_db()
self.ks5 = RSForm(self.operation5.result) self.ks5 = RSForm(self.operation5.result)
self.ks5D4 = self.ks5.insert_new( self.ks5D4 = self.ks5.insert_last(
alias='D4', alias='D4',
definition_formal=r'X1 X2 X3 S1 D1 D2 D3', definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
convention='KS5D4' convention='KS5D4'
@ -114,17 +113,17 @@ class TestChangeOperations(EndpointTester):
{'nodeID': 'o' + str(self.operation4.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.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
def test_oss_setup(self): def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituentsQ().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3) self.assertEqual(self.ks2.constituentsQ().count(), 3)
self.assertEqual(self.ks3.constituents().count(), 2) self.assertEqual(self.ks3.constituentsQ().count(), 2)
self.assertEqual(self.ks4.constituents().count(), 6) self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1') self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@ -142,8 +141,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 4) self.assertEqual(self.ks4.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 6) self.assertEqual(self.ks5.constituentsQ().count(), 6)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@ -166,8 +165,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 4) self.assertEqual(self.ks4.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 6) self.assertEqual(self.ks5.constituentsQ().count(), 6)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@ -180,9 +179,9 @@ class TestChangeOperations(EndpointTester):
title='Test6', title='Test6',
owner=self.user owner=self.user
) )
ks6X1 = ks6.insert_new('X1', convention='KS6X1') ks6X1 = ks6.insert_last('X1', convention='KS6X1')
ks6X2 = ks6.insert_new('X2', convention='KS6X2') ks6X2 = ks6.insert_last('X2', convention='KS6X2')
ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1') ks6D1 = ks6.insert_last('D1', definition_formal='X1 X2', convention='KS6D1')
data = { data = {
'layout': self.layout_data, 'layout': self.layout_data,
@ -201,8 +200,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 7) self.assertEqual(self.ks4.constituentsQ().count(), 7)
self.assertEqual(self.ks5.constituents().count(), 9) self.assertEqual(self.ks5.constituentsQ().count(), 9)
self.assertEqual(ks4Dks6.definition_formal, r'X5 X6') self.assertEqual(ks4Dks6.definition_formal, r'X5 X6')
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1')
@ -220,8 +219,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 0) self.assertEqual(subs3_4.count(), 0)
self.assertEqual(self.ks4.constituents().count(), 4) self.assertEqual(self.ks4.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 7) self.assertEqual(self.ks5.constituentsQ().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL') 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') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@ -242,8 +241,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 0) self.assertEqual(subs3_4.count(), 0)
self.assertEqual(self.ks4.constituents().count(), 4) self.assertEqual(self.ks4.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 7) self.assertEqual(self.ks5.constituentsQ().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL') 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') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 DEL D2 D3')
@ -264,8 +263,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 6) self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1') 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.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')
@ -280,16 +279,16 @@ class TestChangeOperations(EndpointTester):
} }
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks4D2.refresh_from_db() self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db() self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions() subs1_2 = self.operation4.getQ_substitutions()
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituentsQ().count(), 3)
self.assertEqual(self.ks4.constituents().count(), 6) self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1') 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.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()) self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1D1.pk).exists())
@ -325,8 +324,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 2) self.assertEqual(subs1_2.count(), 2)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 5) self.assertEqual(self.ks4.constituentsQ().count(), 5)
self.assertEqual(self.ks5.constituents().count(), 7) self.assertEqual(self.ks5.constituentsQ().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'X1 D1 X3 S1 D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
@ -351,8 +350,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 4) self.assertEqual(self.ks4.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituents().count(), 6) self.assertEqual(self.ks5.constituentsQ().count(), 6)
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@ -364,8 +363,8 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks4.constituents().count(), 7) self.assertEqual(self.ks4.constituentsQ().count(), 7)
self.assertEqual(self.ks5.constituents().count(), 9) self.assertEqual(self.ks5.constituentsQ().count(), 9)
self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
@ -374,9 +373,9 @@ class TestChangeOperations(EndpointTester):
def test_execute_middle_operation(self): def test_execute_middle_operation(self):
self.client.delete(f'/api/library/{self.ks4.model.pk}') self.client.delete(f'/api/library/{self.ks4.model.pk}')
self.operation4.refresh_from_db() self.operation4.refresh_from_db()
self.ks5.refresh_from_db() self.ks5.model.refresh_from_db()
self.assertEqual(self.operation4.result, None) self.assertEqual(self.operation4.result, None)
self.assertEqual(self.ks5.constituents().count(), 3) self.assertEqual(self.ks5.constituentsQ().count(), 3)
data = { data = {
'target': self.operation4.pk, 'target': self.operation4.pk,
@ -384,15 +383,15 @@ class TestChangeOperations(EndpointTester):
} }
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.operation4.refresh_from_db() self.operation4.refresh_from_db()
self.ks5.refresh_from_db() self.ks5.model.refresh_from_db()
self.assertNotEqual(self.operation4.result, None) self.assertNotEqual(self.operation4.result, None)
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_up(self): def test_relocate_constituents_up(self):
ks1_old_count = self.ks1.constituents().count() ks1_old_count = self.ks1.constituentsQ().count()
ks4_old_count = self.ks4.constituents().count() ks4_old_count = self.ks4.constituentsQ().count()
operation6 = self.owned.create_operation( operation6 = self.owned.create_operation(
alias='6', alias='6',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
@ -401,8 +400,8 @@ class TestChangeOperations(EndpointTester):
self.owned.execute_operation(operation6) self.owned.execute_operation(operation6)
operation6.refresh_from_db() operation6.refresh_from_db()
ks6 = RSForm(operation6.result) ks6 = RSForm(operation6.result)
ks6A1 = ks6.insert_new('A1') ks6A1 = ks6.insert_last('A1')
ks6_old_count = ks6.constituents().count() ks6_old_count = ks6.constituentsQ().count()
data = { data = {
'destination': self.ks1.model.pk, 'destination': self.ks1.model.pk,
@ -410,19 +409,19 @@ class TestChangeOperations(EndpointTester):
} }
self.executeOK(data=data) self.executeOK(data=data)
ks6.refresh_from_db() ks6.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks4.refresh_from_db() self.ks4.model.refresh_from_db()
self.assertEqual(ks6.constituents().count(), ks6_old_count) self.assertEqual(ks6.constituentsQ().count(), ks6_old_count)
self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1) self.assertEqual(self.ks1.constituentsQ().count(), ks1_old_count + 1)
self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1) self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1)
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents_down(self): def test_relocate_constituents_down(self):
ks1_old_count = self.ks1.constituents().count() ks1_old_count = self.ks1.constituentsQ().count()
ks4_old_count = self.ks4.constituents().count() ks4_old_count = self.ks4.constituentsQ().count()
operation6 = self.owned.create_operation( operation6 = self.owned.create_operation(
alias='6', alias='6',
@ -432,7 +431,7 @@ class TestChangeOperations(EndpointTester):
self.owned.execute_operation(operation6) self.owned.execute_operation(operation6)
operation6.refresh_from_db() operation6.refresh_from_db()
ks6 = RSForm(operation6.result) ks6 = RSForm(operation6.result)
ks6_old_count = ks6.constituents().count() ks6_old_count = ks6.constituentsQ().count()
data = { data = {
'destination': ks6.model.pk, 'destination': ks6.model.pk,
@ -440,14 +439,14 @@ class TestChangeOperations(EndpointTester):
} }
self.executeOK(data=data) self.executeOK(data=data)
ks6.refresh_from_db() ks6.model.refresh_from_db()
self.ks1.refresh_from_db() self.ks1.model.refresh_from_db()
self.ks4.refresh_from_db() self.ks4.model.refresh_from_db()
self.ks4D2.refresh_from_db() self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db() self.ks5D4.refresh_from_db()
self.assertEqual(ks6.constituents().count(), ks6_old_count) self.assertEqual(ks6.constituentsQ().count(), ks6_old_count)
self.assertEqual(self.ks1.constituents().count(), ks1_old_count - 1) self.assertEqual(self.ks1.constituentsQ().count(), ks1_old_count - 1)
self.assertEqual(self.ks4.constituents().count(), ks4_old_count - 1) self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count - 1)
self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3')

View File

@ -0,0 +1,146 @@
''' 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

View File

@ -23,18 +23,18 @@ class TestChangeSubstitutions(EndpointTester):
title='Test1', title='Test1',
owner=self.user owner=self.user
) )
self.ks1X1 = self.ks1.insert_new('X1', convention='KS1X1') self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
self.ks1X2 = self.ks1.insert_new('X2', convention='KS1X2') self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
self.ks1D1 = self.ks1.insert_new('D1', definition_formal='X1 X2', convention='KS1D1') self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
self.ks2 = RSForm.create( self.ks2 = RSForm.create(
alias='KS2', alias='KS2',
title='Test2', title='Test2',
owner=self.user owner=self.user
) )
self.ks2X1 = self.ks2.insert_new('X1', convention='KS2X1') self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
self.ks2X2 = self.ks2.insert_new('X2', convention='KS2X2') self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
self.ks2S1 = self.ks2.insert_new( self.ks2S1 = self.ks2.insert_last(
alias='S1', alias='S1',
definition_formal=r'X1', definition_formal=r'X1',
convention='KS2S1' convention='KS2S1'
@ -45,8 +45,8 @@ class TestChangeSubstitutions(EndpointTester):
title='Test3', title='Test3',
owner=self.user owner=self.user
) )
self.ks3X1 = self.ks3.insert_new('X1', convention='KS3X1') self.ks3X1 = self.ks3.insert_last('X1', convention='KS3X1')
self.ks3D1 = self.ks3.insert_new( self.ks3D1 = self.ks3.insert_last(
alias='D1', alias='D1',
definition_formal='X1 X1', definition_formal='X1 X1',
convention='KS3D1' convention='KS3D1'
@ -83,7 +83,7 @@ class TestChangeSubstitutions(EndpointTester):
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) 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.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk) self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
self.ks4D2 = self.ks4.insert_new( self.ks4D2 = self.ks4.insert_last(
alias='D2', alias='D2',
definition_formal=r'X1 X2 X3 S1 D1', definition_formal=r'X1 X2 X3 S1 D1',
convention='KS4D2' convention='KS4D2'
@ -101,7 +101,7 @@ class TestChangeSubstitutions(EndpointTester):
self.owned.execute_operation(self.operation5) self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db() self.operation5.refresh_from_db()
self.ks5 = RSForm(self.operation5.result) self.ks5 = RSForm(self.operation5.result)
self.ks5D4 = self.ks5.insert_new( self.ks5D4 = self.ks5.insert_last(
alias='D4', alias='D4',
definition_formal=r'X1 X2 X3 S1 D1 D2 D3', definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
convention='KS5D4' 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.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.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
def test_oss_setup(self): def test_oss_setup(self):
self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks1.constituentsQ().count(), 3)
self.assertEqual(self.ks2.constituents().count(), 3) self.assertEqual(self.ks2.constituentsQ().count(), 3)
self.assertEqual(self.ks3.constituents().count(), 2) self.assertEqual(self.ks3.constituentsQ().count(), 2)
self.assertEqual(self.ks4.constituents().count(), 6) self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituents().count(), 8) self.assertEqual(self.ks5.constituentsQ().count(), 8)
self.assertEqual(self.ks4D1.definition_formal, 'S1 X1') self.assertEqual(self.ks4D1.definition_formal, 'S1 X1')
@ -186,7 +186,7 @@ class TestChangeSubstitutions(EndpointTester):
self.assertEqual(subs1_2.count(), 0) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks5.constituents().count(), 7) self.assertEqual(self.ks5.constituentsQ().count(), 7)
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 DEL') 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') 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) self.assertEqual(subs1_2.count(), 0)
subs3_4 = self.operation5.getQ_substitutions() subs3_4 = self.operation5.getQ_substitutions()
self.assertEqual(subs3_4.count(), 1) self.assertEqual(subs3_4.count(), 1)
self.assertEqual(self.ks5.constituents().count(), 7) self.assertEqual(self.ks5.constituentsQ().count(), 7)
self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') self.assertEqual(self.ks4D1.definition_formal, r'X4 X1')
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 DEL DEL D1') 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') self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL X3 DEL D1 D2 D3')

View File

@ -1,7 +1,6 @@
''' Testing API: Operation Schema - blocks manipulation. ''' ''' Testing API: Operation Schema - blocks manipulation. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -57,7 +56,7 @@ class TestOssBlocks(EndpointTester):
{'nodeID': 'b' + str(self.block2.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5}, {'nodeID': 'b' + str(self.block2.pk), 'x': 0, 'y': 0, 'width': 0.5, 'height': 0.5},
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()

View File

@ -1,6 +1,6 @@
''' Testing API: Operation Schema - operations manipulation. ''' ''' Testing API: Operation Schema - operations manipulation. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Operation, OperationSchema, OperationType from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Reference
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint from shared.EndpointTester import EndpointTester, decl_endpoint
@ -24,7 +24,7 @@ class TestOssOperations(EndpointTester):
title='Test1', title='Test1',
owner=self.user owner=self.user
) )
self.ks1X1 = self.ks1.insert_new( self.ks1X1 = self.ks1.insert_last(
'X1', 'X1',
term_raw='X1_1', term_raw='X1_1',
term_resolved='X1_1' term_resolved='X1_1'
@ -34,7 +34,7 @@ class TestOssOperations(EndpointTester):
title='Test2', title='Test2',
owner=self.user owner=self.user
) )
self.ks2X1 = self.ks2.insert_new( self.ks2X1 = self.ks2.insert_last(
'X2', 'X2',
term_raw='X1_2', term_raw='X1_2',
term_resolved='X1_2' term_resolved='X1_2'
@ -54,12 +54,17 @@ class TestOssOperations(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.unowned_operation = self.unowned.create_operation(
alias='42',
operation_type=OperationType.INPUT,
result=None
)
self.layout_data = [ self.layout_data = [
{'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}, {'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.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.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
@ -69,7 +74,6 @@ class TestOssOperations(EndpointTester):
'substitution': self.ks2X1 'substitution': self.ks2X1
}]) }])
@decl_endpoint('/api/oss/{item}/create-schema', method='post') @decl_endpoint('/api/oss/{item}/create-schema', method='post')
def test_create_schema(self): def test_create_schema(self):
self.populateData() self.populateData()
@ -123,6 +127,53 @@ class TestOssOperations(EndpointTester):
self.executeCreated(data=data, item=self.unowned_id) 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') @decl_endpoint('/api/oss/{item}/create-schema', method='post')
def test_create_schema_parent(self): def test_create_schema_parent(self):
self.populateData() self.populateData()
@ -158,6 +209,37 @@ class TestOssOperations(EndpointTester):
self.assertEqual(new_operation['parent'], block_owned.id) 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') @decl_endpoint('/api/oss/{item}/create-synthesis', method='post')
def test_create_synthesis(self): def test_create_synthesis(self):
self.populateData() self.populateData()
@ -179,10 +261,10 @@ class TestOssOperations(EndpointTester):
'substitutions': [] 'substitutions': []
} }
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
new_operation_id = response.data['new_operation'] new_operation_id = response.data['new_operation']
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
arguments = self.owned.arguments() arguments = Argument.objects.filter(operation__oss=self.owned.model)
self.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation1)) 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.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation3))
self.assertNotEqual(new_operation['result'], None) self.assertNotEqual(new_operation['result'], None)
@ -199,6 +281,9 @@ class TestOssOperations(EndpointTester):
} }
self.executeBadData(data=data) self.executeBadData(data=data)
data['target'] = self.unowned_operation.pk
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation1.pk data['target'] = self.operation1.pk
self.toggle_admin(True) self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id) self.executeBadData(data=data, item=self.unowned_id)
@ -213,6 +298,39 @@ class TestOssOperations(EndpointTester):
self.assertEqual(len(deleted_items), 0) 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') @decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self): def test_create_input(self):
self.populateData() self.populateData()
@ -248,6 +366,9 @@ class TestOssOperations(EndpointTester):
data['target'] = self.operation3.pk data['target'] = self.operation3.pk
self.executeBadData(data=data) 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') @decl_endpoint('/api/oss/{item}/set-input', method='patch')
def test_set_input_null(self): def test_set_input_null(self):
@ -275,7 +396,7 @@ class TestOssOperations(EndpointTester):
self.ks1.model.alias = 'Test42' self.ks1.model.alias = 'Test42'
self.ks1.model.title = 'Test421' self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42' self.ks1.model.description = 'TestComment42'
self.ks1.save() self.ks1.model.save()
response = self.executeOK(data=data) response = self.executeOK(data=data)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model) self.assertEqual(self.operation1.result, self.ks1.model)
@ -325,7 +446,7 @@ class TestOssOperations(EndpointTester):
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user) ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1') ks3x1 = ks3.insert_last('X1', term_resolved='X1_1')
data = { data = {
'target': self.operation3.pk, 'target': self.operation3.pk,
@ -368,6 +489,10 @@ class TestOssOperations(EndpointTester):
data['layout'] = self.layout_data data['layout'] = self.layout_data
self.executeOK(data=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') @decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self): def test_update_operation_sync(self):
@ -375,7 +500,7 @@ class TestOssOperations(EndpointTester):
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
data = { data = {
'target': self.operation1.pk, 'target': self.unowned_operation.pk,
'item_data': { 'item_data': {
'alias': 'Test3 mod', 'alias': 'Test3 mod',
'title': 'Test title mod', 'title': 'Test title mod',
@ -383,7 +508,9 @@ class TestOssOperations(EndpointTester):
}, },
'layout': self.layout_data 'layout': self.layout_data
} }
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation1.pk
response = self.executeOK(data=data) response = self.executeOK(data=data)
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias']) self.assertEqual(self.operation1.alias, data['item_data']['alias'])
@ -393,12 +520,17 @@ class TestOssOperations(EndpointTester):
self.assertEqual(self.operation1.result.title, data['item_data']['title']) self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.description, data['item_data']['description']) 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') @decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_invalid_substitution(self): def test_update_operation_invalid_substitution(self):
self.populateData() self.populateData()
self.ks1X2 = self.ks1.insert_new('X2') self.ks1X2 = self.ks1.insert_last('X2')
data = { data = {
'target': self.operation3.pk, 'target': self.operation3.pk,
@ -434,6 +566,9 @@ class TestOssOperations(EndpointTester):
} }
self.executeBadData(data=data) self.executeBadData(data=data)
data['target'] = self.unowned_operation.pk
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation3.pk data['target'] = self.operation3.pk
self.toggle_admin(True) self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id) self.executeBadData(data=data, item=self.unowned_id)
@ -448,7 +583,7 @@ class TestOssOperations(EndpointTester):
self.assertEqual(schema.description, self.operation3.description) self.assertEqual(schema.description, self.operation3.description)
self.assertEqual(schema.title, self.operation3.title) self.assertEqual(schema.title, self.operation3.title)
self.assertEqual(schema.visible, False) self.assertEqual(schema.visible, False)
items = list(RSForm(schema).constituents()) items = list(RSForm(schema).constituentsQ())
self.assertEqual(len(items), 1) self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1') self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved) self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved)
@ -540,6 +675,7 @@ class TestOssOperations(EndpointTester):
self.assertEqual(schema.access_policy, self.owned.model.access_policy) self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location) self.assertEqual(schema.location, self.owned.model.location)
@decl_endpoint('/api/oss/{item}/import-schema', method='post') @decl_endpoint('/api/oss/{item}/import-schema', method='post')
def test_import_schema_bad_data(self): def test_import_schema_bad_data(self):
self.populateData() self.populateData()

View File

@ -25,7 +25,7 @@ class TestOssViewset(EndpointTester):
title='Test1', title='Test1',
owner=self.user owner=self.user
) )
self.ks1X1 = self.ks1.insert_new( self.ks1X1 = self.ks1.insert_last(
'X1', 'X1',
term_raw='X1_1', term_raw='X1_1',
term_resolved='X1_1' term_resolved='X1_1'
@ -35,7 +35,7 @@ class TestOssViewset(EndpointTester):
title='Test2', title='Test2',
owner=self.user owner=self.user
) )
self.ks2X1 = self.ks2.insert_new( self.ks2X1 = self.ks2.insert_last(
'X2', 'X2',
term_raw='X1_2', term_raw='X1_2',
term_resolved='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.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.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
] ]
layout = self.owned.layout() layout = OperationSchema.layoutQ(self.owned_id)
layout.data = self.layout_data layout.data = self.layout_data
layout.save() layout.save()
@ -138,8 +138,8 @@ class TestOssViewset(EndpointTester):
self.toggle_admin(False) self.toggle_admin(False)
self.executeOK(data=data, item=self.owned_id) self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.assertEqual(self.owned.layout().data, data['data']) self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data'])
self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.private_id) self.executeForbidden(data=data, item=self.private_id)
@ -148,7 +148,7 @@ class TestOssViewset(EndpointTester):
@decl_endpoint('/api/oss/get-predecessor', method='post') @decl_endpoint('/api/oss/get-predecessor', method='post')
def test_get_predecessor(self): def test_get_predecessor(self):
self.populateData() self.populateData()
self.ks1X2 = self.ks1.insert_new('X2') self.ks1X2 = self.ks1.insert_last('X2')
self.owned.execute_operation(self.operation3) self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
@ -223,13 +223,13 @@ class TestOssViewset(EndpointTester):
@decl_endpoint('/api/oss/relocate-constituents', method='post') @decl_endpoint('/api/oss/relocate-constituents', method='post')
def test_relocate_constituents(self): def test_relocate_constituents(self):
self.populateData() self.populateData()
self.ks1X2 = self.ks1.insert_new('X2', convention='test') self.ks1X2 = self.ks1.insert_last('X2', convention='test')
self.owned.execute_operation(self.operation3) self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db() self.operation3.refresh_from_db()
self.ks3 = RSForm(self.operation3.result) self.ks3 = RSForm(self.operation3.result)
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
self.ks3X10 = self.ks3.insert_new('X10', convention='test2') self.ks3X10 = self.ks3.insert_last('X10', convention='test2')
# invalid destination # invalid destination
data = { data = {

View File

@ -14,7 +14,7 @@ from rest_framework.response import Response
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from apps.rsform.models import Constituenta, RSForm from apps.rsform.models import Constituenta, RSFormCached
from apps.rsform.serializers import CstTargetSerializer from apps.rsform.serializers import CstTargetSerializer
from shared import messages as msg from shared import messages as msg
from shared import permissions from shared import permissions
@ -25,7 +25,6 @@ from .. import serializers as s
def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem: def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem:
''' Create clone of prototype schema for operation. ''' ''' Create clone of prototype schema for operation. '''
prototype_schema = RSForm(prototype)
clone = deepcopy(prototype) clone = deepcopy(prototype)
clone.pk = None clone.pk = None
clone.owner = oss.owner clone.owner = oss.owner
@ -37,7 +36,7 @@ def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryIt
clone.access_policy = oss.access_policy clone.access_policy = oss.access_policy
clone.location = oss.location clone.location = oss.location
clone.save() clone.save()
for cst in prototype_schema.constituents(): for cst in Constituenta.objects.filter(schema_id=prototype.pk):
cst_copy = deepcopy(cst) cst_copy = deepcopy(cst)
cst_copy.pk = None cst_copy.pk = None
cst_copy.schema = clone cst_copy.schema = clone
@ -64,10 +63,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'delete_block', 'delete_block',
'move_items', 'move_items',
'create_schema', 'create_schema',
'clone_schema',
'import_schema', 'import_schema',
'create_reference',
'create_synthesis', 'create_synthesis',
'update_operation', 'update_operation',
'delete_operation', 'delete_operation',
'delete_reference',
'create_input', 'create_input',
'set_input', 'set_input',
'execute_operation', 'execute_operation',
@ -105,7 +107,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
tags=['OSS'], tags=['OSS'],
request=s.LayoutSerializer, request=s.LayoutSerializer,
responses={ responses={
c.HTTP_200_OK: None, c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_403_FORBIDDEN: None, c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
@ -115,8 +117,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
''' Endpoint: Update schema layout. ''' ''' Endpoint: Update schema layout. '''
serializer = s.LayoutSerializer(data=request.data) serializer = s.LayoutSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
m.OperationSchema(self.get_object()).update_layout(serializer.validated_data['data']) item = self._get_item()
return Response(status=c.HTTP_200_OK)
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)
@extend_schema( @extend_schema(
summary='create block', summary='create block',
@ -132,18 +139,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-block') @action(detail=True, methods=['post'], url_path='create-block')
def create_block(self, request: Request, pk) -> HttpResponse: def create_block(self, request: Request, pk) -> HttpResponse:
''' Create Block. ''' ''' Create Block. '''
item = self._get_item()
serializer = s.CreateBlockSerializer( serializer = s.CreateBlockSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
position = serializer.validated_data['position'] position = serializer.validated_data['position']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks'] children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations'] children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchema(item)
new_block = oss.create_block(**serializer.validated_data['item_data']) new_block = oss.create_block(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'b' + str(new_block.pk), 'nodeID': 'b' + str(new_block.pk),
@ -152,7 +160,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'width': position['width'], 'width': position['width'],
'height': position['height'], 'height': position['height'],
}) })
oss.update_layout(layout) m.Layout.update_data(pk, layout)
if len(children_blocks) > 0: if len(children_blocks) > 0:
for block in children_blocks: for block in children_blocks:
block.parent = new_block block.parent = new_block
@ -161,12 +169,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
for operation in children_operations: for operation in children_operations:
operation.parent = new_block operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent']) m.Operation.objects.bulk_update(children_operations, ['parent'])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_block': new_block.pk, 'new_block': new_block.pk,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(item).data
} }
) )
@ -184,17 +193,15 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='update-block') @action(detail=True, methods=['patch'], url_path='update-block')
def update_block(self, request: Request, pk) -> HttpResponse: def update_block(self, request: Request, pk) -> HttpResponse:
''' Update Block. ''' ''' Update Block. '''
item = self._get_item()
serializer = s.UpdateBlockSerializer( serializer = s.UpdateBlockSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) 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(): with transaction.atomic():
if 'layout' in serializer.validated_data:
oss.update_layout(serializer.validated_data['layout'])
if 'title' in serializer.validated_data['item_data']: if 'title' in serializer.validated_data['item_data']:
block.title = serializer.validated_data['item_data']['title'] block.title = serializer.validated_data['item_data']['title']
if 'description' in serializer.validated_data['item_data']: if 'description' in serializer.validated_data['item_data']:
@ -202,9 +209,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if 'parent' in serializer.validated_data['item_data']: if 'parent' in serializer.validated_data['item_data']:
block.parent = serializer.validated_data['item_data']['parent'] block.parent = serializer.validated_data['item_data']['parent']
block.save(update_fields=['title', 'description', '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( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -221,23 +233,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-block') @action(detail=True, methods=['patch'], url_path='delete-block')
def delete_block(self, request: Request, pk) -> HttpResponse: def delete_block(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Block. ''' ''' Endpoint: Delete Block. '''
item = self._get_item()
serializer = s.DeleteBlockSerializer( serializer = s.DeleteBlockSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
block = cast(m.Block, serializer.validated_data['target']) block = cast(m.Block, serializer.validated_data['target'])
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)] layout = [x for x in layout if x['nodeID'] != 'b' + str(block.pk)]
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchema(item)
oss.delete_block(block) oss.delete_block(block)
oss.update_layout(layout) m.Layout.update_data(pk, layout)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -254,25 +268,27 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='move-items') @action(detail=True, methods=['patch'], url_path='move-items')
def move_items(self, request: Request, pk) -> HttpResponse: def move_items(self, request: Request, pk) -> HttpResponse:
''' Move items to another parent. ''' ''' Move items to another parent. '''
item = self._get_item()
serializer = s.MoveItemsSerializer( serializer = s.MoveItemsSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_layout(serializer.validated_data['layout']) m.Layout.update_data(pk, layout)
for operation in serializer.validated_data['operations']: for operation in serializer.validated_data['operations']:
operation.parent = serializer.validated_data['destination'] operation.parent = serializer.validated_data['destination']
operation.save(update_fields=['parent']) operation.save(update_fields=['parent'])
for block in serializer.validated_data['blocks']: for block in serializer.validated_data['blocks']:
block.parent = serializer.validated_data['destination'] block.parent = serializer.validated_data['destination']
block.save(update_fields=['parent']) block.save(update_fields=['parent'])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -289,18 +305,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-schema') @action(detail=True, methods=['post'], url_path='create-schema')
def create_schema(self, request: Request, pk) -> HttpResponse: def create_schema(self, request: Request, pk) -> HttpResponse:
''' Create schema. ''' ''' Create schema. '''
item = self._get_item()
serializer = s.CreateSchemaSerializer( serializer = s.CreateSchemaSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
position = serializer.validated_data['position'] position = serializer.validated_data['position']
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
data['operation_type'] = m.OperationType.INPUT data['operation_type'] = m.OperationType.INPUT
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchema(item)
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'o' + str(new_operation.pk), 'nodeID': 'o' + str(new_operation.pk),
@ -309,14 +326,85 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'width': position['width'], 'width': position['width'],
'height': position['height'] 'height': position['height']
}) })
oss.update_layout(layout) m.Layout.update_data(pk, layout)
oss.create_input(new_operation) oss.create_input(new_operation)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': new_operation.pk, 'new_operation': new_operation.pk,
'oss': s.OperationSchemaSerializer(oss.model).data '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
} }
) )
@ -335,20 +423,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='import-schema') @action(detail=True, methods=['post'], url_path='import-schema')
def import_schema(self, request: Request, pk) -> HttpResponse: def import_schema(self, request: Request, pk) -> HttpResponse:
''' Create operation with existing schema. ''' ''' Create operation with existing schema. '''
item = self._get_item()
serializer = s.ImportSchemaSerializer( serializer = s.ImportSchemaSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
position = serializer.validated_data['position'] position = serializer.validated_data['position']
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
data['operation_type'] = m.OperationType.INPUT data['operation_type'] = m.OperationType.INPUT
if not serializer.validated_data['clone_source']: if not serializer.validated_data['clone_source']:
data['result'] = serializer.validated_data['source'] data['result'] = serializer.validated_data['source']
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchema(item)
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'o' + str(new_operation.pk), 'nodeID': 'o' + str(new_operation.pk),
@ -357,18 +446,66 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'width': position['width'], 'width': position['width'],
'height': position['height'] 'height': position['height']
}) })
oss.update_layout(layout) m.Layout.update_data(pk, layout)
if serializer.validated_data['clone_source']: if serializer.validated_data['clone_source']:
prototype: LibraryItem = serializer.validated_data['source'] prototype: LibraryItem = serializer.validated_data['source']
new_operation.result = _create_clone(prototype, new_operation, oss.model) new_operation.result = _create_clone(prototype, new_operation, item)
new_operation.save(update_fields=["result"]) new_operation.save(update_fields=["result"])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': new_operation.pk, 'new_operation': new_operation.pk,
'oss': s.OperationSchemaSerializer(oss.model).data '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
} }
) )
@ -386,18 +523,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='create-synthesis') @action(detail=True, methods=['post'], url_path='create-synthesis')
def create_synthesis(self, request: Request, pk) -> HttpResponse: def create_synthesis(self, request: Request, pk) -> HttpResponse:
''' Create Synthesis operation from arguments. ''' ''' Create Synthesis operation from arguments. '''
item = self._get_item()
serializer = s.CreateSynthesisSerializer( serializer = s.CreateSynthesisSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
position = serializer.validated_data['position'] position = serializer.validated_data['position']
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
data['operation_type'] = m.OperationType.SYNTHESIS data['operation_type'] = m.OperationType.SYNTHESIS
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchema(item)
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'o' + str(new_operation.pk), 'nodeID': 'o' + str(new_operation.pk),
@ -409,13 +547,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.set_arguments(new_operation.pk, serializer.validated_data['arguments']) oss.set_arguments(new_operation.pk, serializer.validated_data['arguments'])
oss.set_substitutions(new_operation.pk, serializer.validated_data['substitutions']) oss.set_substitutions(new_operation.pk, serializer.validated_data['substitutions'])
oss.execute_operation(new_operation) oss.execute_operation(new_operation)
oss.update_layout(layout) m.Layout.update_data(pk, layout)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': new_operation.pk, 'new_operation': new_operation.pk,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(item).data
} }
) )
@ -433,17 +572,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='update-operation') @action(detail=True, methods=['patch'], url_path='update-operation')
def update_operation(self, request: Request, pk) -> HttpResponse: def update_operation(self, request: Request, pk) -> HttpResponse:
''' Update Operation arguments and parameters. ''' ''' Update Operation arguments and parameters. '''
item = self._get_item()
serializer = s.UpdateOperationSerializer( serializer = s.UpdateOperationSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) 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(): with transaction.atomic():
oss = m.OperationSchemaCached(item)
if 'layout' in serializer.validated_data: if 'layout' in serializer.validated_data:
oss.update_layout(serializer.validated_data['layout']) layout = serializer.validated_data['layout']
m.Layout.update_data(pk, layout)
if 'alias' in serializer.validated_data['item_data']: if 'alias' in serializer.validated_data['item_data']:
operation.alias = serializer.validated_data['item_data']['alias'] operation.alias = serializer.validated_data['item_data']['alias']
if 'title' in serializer.validated_data['item_data']: if 'title' in serializer.validated_data['item_data']:
@ -465,9 +606,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.set_arguments(operation.pk, serializer.validated_data['arguments']) oss.set_arguments(operation.pk, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data: if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation.pk, serializer.validated_data['substitutions']) oss.set_substitutions(operation.pk, serializer.validated_data['substitutions'])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -484,30 +627,68 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation') @action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk) -> HttpResponse: def delete_operation(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Operation. ''' ''' Endpoint: Delete Operation. '''
item = self._get_item()
serializer = s.DeleteOperationSerializer( serializer = s.DeleteOperationSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
oss = m.OperationSchema(self.get_object())
operation = cast(m.Operation, serializer.validated_data['target']) operation = cast(m.Operation, serializer.validated_data['target'])
old_schema = operation.result old_schema = operation.result
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)] layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item)
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
oss.update_layout(layout) m.Layout.update_data(pk, layout)
if old_schema is not None: if old_schema is not None:
if serializer.validated_data['delete_schema']: if serializer.validated_data['delete_schema']:
m.PropagationFacade.before_delete_schema(old_schema) m.PropagationFacade.before_delete_schema(old_schema)
old_schema.delete() old_schema.delete()
elif old_schema.is_synced(oss.model): elif old_schema.is_synced(item):
old_schema.visible = True old_schema.visible = True
old_schema.save(update_fields=['visible']) old_schema.save(update_fields=['visible'])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data 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
) )
@extend_schema( @extend_schema(
@ -524,12 +705,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='create-input') @action(detail=True, methods=['patch'], url_path='create-input')
def create_input(self, request: Request, pk) -> HttpResponse: def create_input(self, request: Request, pk) -> HttpResponse:
''' Create input RSForm. ''' ''' Create input RSForm. '''
item = self._get_item()
serializer = s.TargetOperationSerializer( serializer = s.TargetOperationSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) 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'])
if len(operation.getQ_arguments()) > 0: if len(operation.getQ_arguments()) > 0:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -539,17 +720,19 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias) 'target': msg.operationResultNotEmpty(operation.alias)
}) })
layout = serializer.validated_data['layout']
oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_layout(serializer.validated_data['layout']) oss = m.OperationSchema(item)
m.Layout.update_data(pk, layout)
schema = oss.create_input(operation) schema = oss.create_input(operation)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'new_schema': LibraryItemSerializer(schema.model).data, 'new_schema': LibraryItemSerializer(schema.model).data,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(item).data
} }
) )
@ -567,12 +750,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='set-input') @action(detail=True, methods=['patch'], url_path='set-input')
def set_input(self, request: Request, pk) -> HttpResponse: def set_input(self, request: Request, pk) -> HttpResponse:
''' Set input schema for target operation. ''' ''' Set input schema for target operation. '''
item = self._get_item()
serializer = s.SetOperationInputSerializer( serializer = s.SetOperationInputSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
target_operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) target_operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
schema: Optional[LibraryItem] = serializer.validated_data['input'] schema: Optional[LibraryItem] = serializer.validated_data['input']
if schema is not None: if schema is not None:
@ -586,18 +771,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
raise serializers.ValidationError({ raise serializers.ValidationError({
'input': msg.operationInputAlreadyConnected() 'input': msg.operationInputAlreadyConnected()
}) })
oss = m.OperationSchema(self.get_object())
old_schema = target_operation.result old_schema = target_operation.result
with transaction.atomic(): with transaction.atomic():
oss = m.OperationSchemaCached(item)
if old_schema is not None: if old_schema is not None:
if old_schema.is_synced(oss.model): if old_schema.is_synced(item):
old_schema.visible = True old_schema.visible = True
old_schema.save(update_fields=['visible']) old_schema.save(update_fields=['visible'])
oss.update_layout(serializer.validated_data['layout']) m.Layout.update_data(pk, layout)
oss.set_input(target_operation.pk, schema) oss.set_input(target_operation.pk, schema)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -614,12 +802,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['post'], url_path='execute-operation') @action(detail=True, methods=['post'], url_path='execute-operation')
def execute_operation(self, request: Request, pk) -> HttpResponse: def execute_operation(self, request: Request, pk) -> HttpResponse:
''' Execute operation. ''' ''' Execute operation. '''
item = self._get_item()
serializer = s.TargetOperationSerializer( serializer = s.TargetOperationSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': item}
) )
serializer.is_valid(raise_exception=True) 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'])
if operation.operation_type != m.OperationType.SYNTHESIS: if operation.operation_type != m.OperationType.SYNTHESIS:
raise serializers.ValidationError({ raise serializers.ValidationError({
@ -629,15 +817,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias) 'target': msg.operationResultNotEmpty(operation.alias)
}) })
layout = serializer.validated_data['layout']
oss = m.OperationSchema(self.get_object())
with transaction.atomic(): with transaction.atomic():
oss.update_layout(serializer.validated_data['layout']) oss = m.OperationSchemaCached(item)
oss.execute_operation(operation) oss.execute_operation(operation)
m.Layout.update_data(pk, layout)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -689,17 +879,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
''' Relocate constituents from one schema to another. ''' ''' Relocate constituents from one schema to another. '''
serializer = s.RelocateConstituentsSerializer(data=request.data) serializer = s.RelocateConstituentsSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
oss = m.OperationSchema(LibraryItem.objects.get(pk=data['oss'])) ids = [cst.pk for cst in data['items']]
source = RSForm(LibraryItem.objects.get(pk=data['source']))
destination = RSForm(LibraryItem.objects.get(pk=data['destination']))
with transaction.atomic(): 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']: if data['move_down']:
oss.relocate_down(source, destination, data['items']) oss.relocate_down(source, destination, ids)
m.PropagationFacade.before_delete_cst(source, data['items']) m.PropagationFacade.before_delete_cst(data['source'], ids)
source.delete_cst(data['items']) source.delete_cst(ids)
else: else:
new_items = oss.relocate_up(source, destination, data['items']) new_items = oss.relocate_up(source, destination, data['items'])
m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk]) m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk])

View File

@ -8,5 +8,5 @@ from . import models
class ConstituentaAdmin(admin.ModelAdmin): class ConstituentaAdmin(admin.ModelAdmin):
''' Admin model: Constituenta. ''' ''' Admin model: Constituenta. '''
ordering = ['schema', 'order'] ordering = ['schema', 'order']
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved'] list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
search_fields = ['term_resolved', 'definition_resolved'] search_fields = ['term_resolved', 'definition_resolved']

View File

@ -0,0 +1,18 @@
# 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='Ключевая'),
),
]

View File

@ -4,6 +4,7 @@ import re
from cctext import extract_entities from cctext import extract_entities
from django.db.models import ( from django.db.models import (
CASCADE, CASCADE,
BooleanField,
CharField, CharField,
ForeignKey, ForeignKey,
JSONField, JSONField,
@ -103,6 +104,10 @@ class Constituenta(Model):
default='', default='',
blank=True blank=True
) )
crucial = BooleanField(
verbose_name='Ключевая',
default=False
)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''

View File

@ -0,0 +1,64 @@
''' 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'])

View File

@ -1,8 +1,9 @@
''' Models: RSForm API. ''' ''' Models: RSForm API. '''
from copy import deepcopy # pylint: disable=duplicate-code
from typing import Iterable, Optional, cast from typing import Iterable, Optional, cast
from cctext import Entity, Resolver, TermForm, extract_entities, split_grams from cctext import Entity, Resolver, TermForm, split_grams
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models import QuerySet from django.db.models import QuerySet
@ -10,28 +11,19 @@ from apps.library.models import LibraryItem, LibraryItemType, Version
from shared import messages as msg from shared import messages as msg
from ..graph import Graph from ..graph import Graph
from .api_RSLanguage import ( from .api_RSLanguage import get_type_prefix, guess_type
generate_structure, from .Constituenta import Constituenta, CstType, extract_entities, extract_globals
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 INSERT_LAST: int = -1
DELETED_ALIAS = 'DEL' DELETED_ALIAS = 'DEL'
class RSForm: class RSForm:
''' RSForm is math form of conceptual schema. ''' ''' RSForm wrapper. No caching, each mutation requires querying. '''
def __init__(self, model: LibraryItem): def __init__(self, model: LibraryItem):
assert model.item_type == LibraryItemType.RSFORM
self.model = model self.model = model
self.cache: RSFormCache = RSFormCache(self)
@staticmethod @staticmethod
def create(**kwargs) -> 'RSForm': def create(**kwargs) -> 'RSForm':
@ -40,40 +32,11 @@ class RSForm:
return RSForm(model) return RSForm(model)
@staticmethod @staticmethod
def from_id(pk: int) -> 'RSForm': def resolver_from_schema(schemaID: int) -> Resolver:
''' 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. ''' ''' Create resolver for text references based on schema terms. '''
result = Resolver({}) result = Resolver({})
for cst in self.constituents().only('alias', 'term_resolved', 'term_forms'): constituents = Constituenta.objects.filter(schema_id=schemaID).only('alias', 'term_resolved', 'term_forms')
for cst in constituents:
entity = Entity( entity = Entity(
alias=cst.alias, alias=cst.alias,
nominal=cst.term_resolved, nominal=cst.term_resolved,
@ -85,23 +48,126 @@ class RSForm:
result.context[cst.alias] = entity result.context[cst.alias] = entity
return result return result
def semantic(self) -> 'SemanticInfo': @staticmethod
''' Access semantic information on constituents. ''' def resolver_from_list(cst_list: Iterable[Constituenta]) -> Resolver:
return SemanticInfo(self) ''' 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 after_term_change(self, changed: list[int]) -> None: @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:
''' Trigger cascade resolutions when term changes. ''' ''' Trigger cascade resolutions when term changes. '''
self.cache.ensure_loaded() if cst_by_alias is None:
graph_terms = self._graph_term() 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)
expansion = graph_terms.expand_outputs(changed) expansion = graph_terms.expand_outputs(changed)
expanded_change = changed + expansion expanded_change = changed + expansion
update_list: list[Constituenta] = [] update_list: list[Constituenta] = []
resolver = self.resolver()
if resolver is None:
resolver = RSForm.resolver_from_list(cst_list)
if len(expansion) > 0: if len(expansion) > 0:
for cst_id in graph_terms.topological_order(): for cst_id in graph_terms.topological_order():
if cst_id not in expansion: if cst_id not in expansion:
continue continue
cst = self.cache.by_id[cst_id] cst = cst_by_id[cst_id]
resolved = resolver.resolve(cst.term_raw) resolved = resolver.resolve(cst.term_raw)
if resolved == resolver.context[cst.alias].get_nominal(): if resolved == resolver.context[cst.alias].get_nominal():
continue continue
@ -110,75 +176,34 @@ class RSForm:
resolver.context[cst.alias] = Entity(cst.alias, resolved) resolver.context[cst.alias] = Entity(cst.alias, resolved)
Constituenta.objects.bulk_update(update_list, ['term_resolved']) Constituenta.objects.bulk_update(update_list, ['term_resolved'])
graph_defs = self._graph_text() graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed) update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
update_list = [] update_list = []
if len(update_defs) == 0: if len(update_defs) == 0:
return return
for cst_id in update_defs: for cst_id in update_defs:
cst = self.cache.by_id[cst_id] cst = cst_by_id[cst_id]
resolved = resolver.resolve(cst.definition_raw) resolved = resolver.resolve(cst.definition_raw)
cst.definition_resolved = resolved cst.definition_resolved = resolved
update_list.append(cst) update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['definition_resolved']) Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
def get_max_index(self, cst_type: str) -> int: def constituentsQ(self) -> QuerySet[Constituenta]:
''' Get maximum alias index for specific CstType. ''' ''' Get QuerySet containing all constituents of current RSForm. '''
result: int = 0 return Constituenta.objects.filter(schema=self.model)
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 create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta: def insert_last(
''' 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, self,
alias: str, alias: str,
cst_type: Optional[CstType] = None, cst_type: Optional[CstType] = None,
position: int = INSERT_LAST,
**kwargs **kwargs
) -> Constituenta: ) -> Constituenta:
''' Insert new constituenta at given position. ''' ''' Insert new constituenta at last position. '''
if self.constituents().filter(alias=alias).exists(): if Constituenta.objects.filter(schema=self.model, alias=alias).exists():
raise ValidationError(msg.aliasTaken(alias)) raise ValidationError(msg.aliasTaken(alias))
position = self._get_insert_position(position)
if cst_type is None: if cst_type is None:
cst_type = guess_type(alias) cst_type = guess_type(alias)
self._shift_positions(position, 1) position = Constituenta.objects.filter(schema=self.model).count()
result = Constituenta.objects.create( result = Constituenta.objects.create(
schema=self.model, schema=self.model,
order=position, order=position,
@ -186,115 +211,16 @@ class RSForm:
cst_type=cst_type, cst_type=cst_type,
**kwargs **kwargs
) )
self.cache.insert(result)
self.save(update_fields=['time_update'])
return result 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: 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_moved = 0
count_top = 0 count_top = 0
count_bot = 0 count_bot = 0
size = len(target) size = len(target)
cst_list: Iterable[Constituenta] = [] cst_list = Constituenta.objects.filter(schema=self.model).only('order').order_by('order')
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: for cst in cst_list:
if cst in target: if cst in target:
cst.order = destination + count_moved cst.order = destination + count_moved
@ -306,99 +232,54 @@ class RSForm:
cst.order = destination + size + count_bot cst.order = destination + size + count_bot
count_bot += 1 count_bot += 1
Constituenta.objects.bulk_update(cst_list, ['order']) Constituenta.objects.bulk_update(cst_list, ['order'])
self.save(update_fields=['time_update'])
def delete_cst(self, target: Iterable[Constituenta]) -> None: def delete_cst(self, target: list[Constituenta]) -> None:
''' Delete multiple constituents. Do not check if listCst are from this schema. ''' ''' Delete multiple constituents. '''
ids = [cst.pk for cst in target]
mapping = {cst.alias: DELETED_ALIAS for cst in target} mapping = {cst.alias: DELETED_ALIAS for cst in target}
self.cache.ensure_loaded() Constituenta.objects.filter(pk__in=ids).delete()
self.cache.remove_multi(target) all_cst = Constituenta.objects.filter(schema=self.model).only(
self.apply_mapping(mapping) 'alias', 'definition_formal', 'term_raw', 'definition_raw', 'order'
Constituenta.objects.filter(pk__in=[cst.pk for cst in target]).delete() ).order_by('order')
self._reset_order() RSForm.apply_mapping(mapping, all_cst, change_aliases=False)
self.save(update_fields=['time_update']) RSForm.save_order(all_cst)
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
''' Execute constituenta substitution. '''
mapping = {}
deleted: list[Constituenta] = []
replacements: list[Constituenta] = []
for original, substitution in substitutions:
mapping[original.alias] = substitution.alias
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: def reset_aliases(self) -> None:
''' Recreate all aliases based on constituents order. ''' ''' Recreate all aliases based on constituents order. '''
mapping = self._create_reset_mapping() bases = cast(dict[str, int], {})
self.apply_mapping(mapping, change_aliases=True) mapping = cast(dict[str, str], {})
for cst_type in CstType.values:
def change_cst_type(self, target: int, new_type: CstType) -> bool: bases[cst_type] = 1
''' Change type of constituenta generating alias automatically. ''' cst_list = Constituenta.objects.filter(schema=self.model).only(
self.cache.ensure_loaded() 'alias', 'cst_type', 'definition_formal',
cst = self.cache.by_id.get(target) 'term_raw', 'definition_raw'
if cst is None: ).order_by('order')
return False for cst in cst_list:
newAlias = f'{get_type_prefix(new_type)}{self.get_max_index(new_type) + 1}' alias = f'{get_type_prefix(cst.cst_type)}{bases[cst.cst_type]}'
mapping = {cst.alias: newAlias} bases[cst.cst_type] += 1
cst.cst_type = new_type if cst.alias != alias:
cst.alias = newAlias mapping[cst.alias] = alias
cst.save(update_fields=['cst_type', 'alias']) RSForm.apply_mapping(mapping, cst_list, change_aliases=True)
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 substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
''' Execute constituenta substitution. '''
if len(substitutions) < 1:
return
mapping = {}
deleted: list[int] = []
replacements: list[int] = []
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)
def create_version(self, version: str, description: str, data) -> Version: def create_version(self, version: str, description: str, data) -> Version:
''' Creates version for current state. ''' ''' Creates version for current state. '''
@ -408,370 +289,3 @@ class RSForm:
description=description, description=description,
data=data 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'])

View File

@ -0,0 +1,438 @@
''' 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]

View File

@ -0,0 +1,136 @@
''' 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

View File

@ -1,4 +1,6 @@
''' Django: Models. ''' ''' Django: Models. '''
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm, SemanticInfo from .OrderManager import OrderManager
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
from .RSFormCached import RSFormCached

View File

@ -12,6 +12,7 @@ from .basics import (
WordFormSerializer WordFormSerializer
) )
from .data_access import ( from .data_access import (
CrucialUpdateSerializer,
CstCreateSerializer, CstCreateSerializer,
CstInfoSerializer, CstInfoSerializer,
CstListSerializer, CstListSerializer,
@ -24,6 +25,6 @@ from .data_access import (
RSFormSerializer, RSFormSerializer,
SubstitutionSerializerBase SubstitutionSerializerBase
) )
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer, generate_trs
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter
from .responses import NewCstResponse, NewMultiCstResponse, ResultTextResponse from .responses import NewCstResponse, NewMultiCstResponse, ResultTextResponse

View File

@ -46,12 +46,16 @@ class CstUpdateSerializer(StrictSerializer):
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = Constituenta model = Constituenta
fields = 'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw', 'term_forms' fields = 'alias', 'cst_type', 'convention', 'crucial', 'definition_formal', \
'definition_raw', 'term_raw', 'term_forms'
target = PKField( target = PKField(
many=False, many=False,
queryset=Constituenta.objects.all().only( queryset=Constituenta.objects.all().only(
'alias', 'cst_type', 'convention', 'definition_formal', 'definition_raw', 'term_raw') 'schema_id',
'alias', 'cst_type', 'convention', 'crucial',
'definition_formal', 'definition_raw', 'term_raw'
)
) )
item_data = ConstituentaUpdateData() item_data = ConstituentaUpdateData()
@ -64,13 +68,31 @@ class CstUpdateSerializer(StrictSerializer):
}) })
if 'alias' in attrs['item_data']: if 'alias' in attrs['item_data']:
new_alias = attrs['item_data']['alias'] new_alias = attrs['item_data']['alias']
if cst.alias != new_alias and RSForm(schema).constituents().filter(alias=new_alias).exists(): if cst.alias != new_alias and Constituenta.objects.filter(schema=schema, alias=new_alias).exists():
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.aliasTaken(new_alias) 'alias': msg.aliasTaken(new_alias)
}) })
return attrs 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): class CstDetailsSerializer(StrictModelSerializer):
''' Serializer: Constituenta data including parse. ''' ''' Serializer: Constituenta data including parse. '''
parse = CstParseSerializer() parse = CstParseSerializer()
@ -96,7 +118,7 @@ class CstCreateSerializer(StrictModelSerializer):
''' serializer metadata. ''' ''' serializer metadata. '''
model = Constituenta model = Constituenta
fields = \ fields = \
'alias', 'cst_type', 'convention', \ 'alias', 'cst_type', 'convention', 'crucial', \
'term_raw', 'definition_raw', 'definition_formal', \ 'term_raw', 'definition_raw', 'definition_formal', \
'insert_after', 'term_forms' 'insert_after', 'term_forms'
@ -142,7 +164,7 @@ class RSFormSerializer(StrictModelSerializer):
result['items'] = [] result['items'] = []
result['oss'] = [] result['oss'] = []
result['inheritance'] = [] result['inheritance'] = []
for cst in RSForm(instance).constituents().defer('order').order_by('order'): for cst in Constituenta.objects.filter(schema=instance).defer('order').order_by('order'):
result['items'].append(CstInfoSerializer(cst).data) result['items'].append(CstInfoSerializer(cst).data)
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'): for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
result['oss'].append({ result['oss'].append({
@ -175,17 +197,18 @@ class RSFormSerializer(StrictModelSerializer):
def restore_from_version(self, data: dict): def restore_from_version(self, data: dict):
''' Load data from version. ''' ''' Load data from version. '''
schema = RSForm(cast(LibraryItem, self.instance)) instance = cast(LibraryItem, self.instance)
schema = RSForm(instance)
items: list[dict] = data['items'] items: list[dict] = data['items']
ids: list[int] = [item['id'] for item in items] ids: list[int] = [item['id'] for item in items]
processed: list[int] = [] processed: list[int] = []
for cst in schema.constituents(): for cst in schema.constituentsQ():
if not cst.pk in ids: if not cst.pk in ids:
cst.delete() cst.delete()
else: else:
cst_data = next(x for x in items if x['id'] == cst.pk) cst_data = next(x for x in items if x['id'] == cst.pk)
cst_data['schema'] = cast(LibraryItem, self.instance).pk cst_data['schema'] = instance.pk
new_cst = CstBaseSerializer(data=cst_data) new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True) new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(cst.pk) new_cst.validated_data['order'] = ids.index(cst.pk)
@ -197,10 +220,10 @@ class RSFormSerializer(StrictModelSerializer):
for cst_data in items: for cst_data in items:
if cst_data['id'] not in processed: if cst_data['id'] not in processed:
cst = schema.insert_new(cst_data['alias']) cst = schema.insert_last(cst_data['alias'])
old_id = cst_data['id'] old_id = cst_data['id']
cst_data['id'] = cst.pk cst_data['id'] = cst.pk
cst_data['schema'] = cast(LibraryItem, self.instance).pk cst_data['schema'] = instance.pk
new_cst = CstBaseSerializer(data=cst_data) new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True) new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(old_id) new_cst.validated_data['order'] = ids.index(old_id)

View File

@ -6,7 +6,7 @@ from apps.library.models import LibraryItem
from shared import messages as msg from shared import messages as msg
from shared.serializers import StrictSerializer from shared.serializers import StrictSerializer
from ..models import Constituenta, RSForm from ..models import Constituenta, RSFormCached
from ..utils import fix_old_references from ..utils import fix_old_references
_CST_TYPE = 'constituenta' _CST_TYPE = 'constituenta'
@ -27,33 +27,12 @@ class RSFormUploadSerializer(StrictSerializer):
load_metadata = serializers.BooleanField() load_metadata = serializers.BooleanField()
class RSFormTRSSerializer(serializers.Serializer): def generate_trs(schema: LibraryItem) -> dict:
''' Serializer: TRS file production and loading for RSForm. ''' ''' Generate TRS file for RSForm. '''
items = []
def to_representation(self, instance: RSForm) -> dict: for cst in Constituenta.objects.filter(schema=schema).order_by('order'):
result = self._prepare_json_rsform(instance.model) items.append(
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, 'entityUID': cst.pk,
'type': _CST_TYPE, 'type': _CST_TYPE,
'cstType': cst.cst_type, 'cstType': cst.cst_type,
@ -72,8 +51,25 @@ class RSFormTRSSerializer(serializers.Serializer):
}, },
}, },
} }
)
return {
'type': _TRS_TYPE,
'title': schema.title,
'alias': schema.alias,
'comment': schema.description,
'items': items,
'claimed': False,
'selection': [],
'version': _TRS_VERSION,
'versionInfo': _TRS_HEADER
}
def from_versioned_data(self, data: dict) -> dict:
class RSFormTRSSerializer(serializers.Serializer):
''' Serializer: TRS file production and loading for RSForm. '''
@staticmethod
def load_versioned_data(data: dict) -> dict:
''' Load data from version. ''' ''' Load data from version. '''
result = { result = {
'type': _TRS_TYPE, 'type': _TRS_TYPE,
@ -127,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer):
result['description'] = data.get('description', '') result['description'] = data.get('description', '')
if 'id' in data: if 'id' in data:
result['id'] = data['id'] result['id'] = data['id']
self.instance = RSForm.from_id(result['id']) self.instance = RSFormCached.from_id(result['id'])
return result return result
def validate(self, attrs: dict): def validate(self, attrs: dict):
@ -140,8 +136,8 @@ class RSFormTRSSerializer(serializers.Serializer):
return attrs return attrs
@transaction.atomic @transaction.atomic
def create(self, validated_data: dict) -> RSForm: def create(self, validated_data: dict) -> RSFormCached:
self.instance: RSForm = RSForm.create( self.instance: RSFormCached = RSFormCached.create(
owner=validated_data.get('owner', None), owner=validated_data.get('owner', None),
alias=validated_data['alias'], alias=validated_data['alias'],
title=validated_data['title'], title=validated_data['title'],
@ -151,7 +147,6 @@ class RSFormTRSSerializer(serializers.Serializer):
access_policy=validated_data['access_policy'], access_policy=validated_data['access_policy'],
location=validated_data['location'] location=validated_data['location']
) )
self.instance.save()
order = 0 order = 0
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
cst = Constituenta( cst = Constituenta(
@ -167,7 +162,7 @@ class RSFormTRSSerializer(serializers.Serializer):
return self.instance return self.instance
@transaction.atomic @transaction.atomic
def update(self, instance: RSForm, validated_data) -> RSForm: def update(self, instance: RSFormCached, validated_data) -> RSFormCached:
if 'alias' in validated_data: if 'alias' in validated_data:
instance.model.alias = validated_data['alias'] instance.model.alias = validated_data['alias']
if 'title' in validated_data: if 'title' in validated_data:
@ -176,7 +171,7 @@ class RSFormTRSSerializer(serializers.Serializer):
instance.model.description = validated_data['description'] instance.model.description = validated_data['description']
order = 0 order = 0
prev_constituents = instance.constituents() prev_constituents = instance.constituentsQ()
loaded_ids = set() loaded_ids = set()
for cst_data in validated_data['items']: for cst_data in validated_data['items']:
uid = int(cst_data['entityUID']) uid = int(cst_data['entityUID'])
@ -204,7 +199,7 @@ class RSFormTRSSerializer(serializers.Serializer):
prev_cst.delete() prev_cst.delete()
instance.resolve_all_text() instance.resolve_all_text()
instance.save() instance.model.save()
return instance return instance
@staticmethod @staticmethod

View File

@ -6,20 +6,20 @@ import pyconcept
from shared import messages as msg from shared import messages as msg
from ..models import RSForm from ..models import Constituenta
class PyConceptAdapter: class PyConceptAdapter:
''' RSForm adapter for interacting with pyconcept module. ''' ''' RSForm adapter for interacting with pyconcept module. '''
def __init__(self, data: Union[RSForm, dict]): def __init__(self, data: Union[int, dict]):
try: try:
if 'items' in cast(dict, data): if 'items' in cast(dict, data):
self.data = self._prepare_request_raw(cast(dict, data)) self.data = self._prepare_request_raw(cast(dict, data))
else: else:
self.data = self._prepare_request(cast(RSForm, data)) self.data = self._prepare_request(cast(int, data))
except TypeError: except TypeError:
self.data = self._prepare_request(cast(RSForm, data)) self.data = self._prepare_request(cast(int, data))
self._checked_data: Optional[dict] = None self._checked_data: Optional[dict] = None
def parse(self) -> dict: def parse(self) -> dict:
@ -30,11 +30,11 @@ class PyConceptAdapter:
raise ValueError(msg.pyconceptFailure()) raise ValueError(msg.pyconceptFailure())
return self._checked_data return self._checked_data
def _prepare_request(self, schema: RSForm) -> dict: def _prepare_request(self, schemaID: int) -> dict:
result: dict = { result: dict = {
'items': [] 'items': []
} }
items = schema.constituents().order_by('order') items = Constituenta.objects.filter(schema_id=schemaID).order_by('order')
for cst in items: for cst in items:
result['items'].append({ result['items'].append({
'entityUID': cst.pk, 'entityUID': cst.pk,

View File

@ -1,3 +1,4 @@
''' Tests for Django Models. ''' ''' Tests for Django Models. '''
from .t_Constituenta import * from .t_Constituenta import *
from .t_RSForm import * from .t_RSForm import *
from .t_RSFormCached import *

View File

@ -14,333 +14,46 @@ class TestRSForm(DBTester):
super().setUp() super().setUp()
self.user1 = User.objects.create(username='User1') self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2') self.user2 = User.objects.create(username='User2')
self.schema = RSForm.create(title='Test')
self.assertNotEqual(self.user1, self.user2) self.assertNotEqual(self.user1, self.user2)
self.schema = RSForm.create(title='Test')
def test_constituents(self): def test_constituents(self):
schema1 = RSForm.create(title='Test1') schema1 = RSForm.create(title='Test1')
schema2 = RSForm.create(title='Test2') schema2 = RSForm.create(title='Test2')
self.assertFalse(schema1.constituents().exists()) self.assertFalse(schema1.constituentsQ().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituentsQ().exists())
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0) Constituenta.objects.create(alias='X1', schema=schema1.model, order=0)
Constituenta.objects.create(alias='X2', schema=schema1.model, order=1) Constituenta.objects.create(alias='X2', schema=schema1.model, order=1)
self.assertTrue(schema1.constituents().exists()) self.assertTrue(schema1.constituentsQ().exists())
self.assertFalse(schema2.constituents().exists()) self.assertFalse(schema2.constituentsQ().exists())
self.assertEqual(schema1.constituents().count(), 2) self.assertEqual(schema1.constituentsQ().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): def test_insert_at_invalid_alias(self):
self.schema.insert_new('X1') self.schema.insert_last('X1')
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.schema.insert_new('X1') self.schema.insert_last('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): def test_insert_last(self):
x1 = self.schema.insert_new('X1') x1 = self.schema.insert_last('X1')
self.assertEqual(x1.order, 0) self.assertEqual(x1.order, 0)
self.assertEqual(x1.schema, self.schema.model) self.assertEqual(x1.schema, self.schema.model)
x2 = self.schema.insert_new('X2') x2 = self.schema.insert_last('X2')
self.assertEqual(x2.order, 1) self.assertEqual(x2.order, 1)
self.assertEqual(x2.schema, self.schema.model) self.assertEqual(x2.schema, self.schema.model)
self.assertEqual(x1.order, 0) 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): def test_reset_aliases(self):
x1 = self.schema.insert_new( x1 = self.schema.insert_last(
alias='X11', alias='X11',
term_raw='человек', term_raw='человек',
term_resolved='человек' term_resolved='человек'
) )
x2 = self.schema.insert_new('X21') x2 = self.schema.insert_last('X21')
d1 = self.schema.insert_new( d1 = self.schema.insert_last(
alias='D11', alias='D11',
definition_formal='X21=X21', definition_formal='X21=X21',
term_raw='@{X21|sing}', term_raw='@{X21|sing}',
@ -360,46 +73,47 @@ class TestRSForm(DBTester):
self.assertEqual(d1.definition_raw, '@{X1|datv}') self.assertEqual(d1.definition_raw, '@{X1|datv}')
self.assertEqual(d1.definition_resolved, 'test') self.assertEqual(d1.definition_resolved, 'test')
def test_move_cst(self):
def test_on_term_change(self): x1 = self.schema.insert_last('X1')
x1 = self.schema.insert_new( x2 = self.schema.insert_last('X2')
alias='X1', d1 = self.schema.insert_last('D1')
term_raw='человек', d2 = self.schema.insert_last('D2')
term_resolved='человек', self.schema.move_cst([x2, d2], 0)
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_raw='очень @{X2|sing}',
definition_resolved='очень сильный человек'
)
x1.term_raw = 'слон'
x1.term_resolved = 'слон'
x1.save()
self.schema.after_term_change([x1.pk])
x1.refresh_from_db() x1.refresh_from_db()
x2.refresh_from_db() x2.refresh_from_db()
x3.refresh_from_db()
d1.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)
self.assertEqual(x1.term_raw, 'слон')
self.assertEqual(x1.term_resolved, 'слон') def test_move_cst_down(self):
self.assertEqual(x1.definition_resolved, 'одному слону') x1 = self.schema.insert_last('X1')
self.assertEqual(x2.definition_resolved, x1.definition_resolved) x2 = self.schema.insert_last('X2')
self.assertEqual(x3.definition_resolved, x1.definition_resolved) self.schema.move_cst([x1], 1)
self.assertEqual(d1.definition_resolved, 'очень сильный слон') 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(
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.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}')

View File

@ -0,0 +1,267 @@
''' 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')

View File

@ -73,12 +73,12 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/details', method='get') @decl_endpoint('/api/rsforms/{item}/details', method='get')
def test_details(self): def test_details(self):
x1 = self.owned.insert_new( x1 = self.owned.insert_last(
alias='X1', alias='X1',
term_raw='человек', term_raw='человек',
term_resolved='человек' term_resolved='человек'
) )
x2 = self.owned.insert_new( x2 = self.owned.insert_last(
alias='X2', alias='X2',
term_raw='@{X1|plur}', term_raw='@{X1|plur}',
term_resolved='люди' term_resolved='люди'
@ -115,7 +115,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/check-expression', method='post') @decl_endpoint('/api/rsforms/{item}/check-expression', method='post')
def test_check_expression(self): def test_check_expression(self):
self.owned.insert_new('X1') self.owned.insert_last('X1')
data = {'expression': 'X1=X1'} data = {'expression': 'X1=X1'}
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
@ -129,7 +129,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post') @decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta(self): def test_check_constituenta(self):
self.owned.insert_new('X1') self.owned.insert_last('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM} data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True) self.assertEqual(response.data['parseResult'], True)
@ -141,7 +141,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post') @decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta_error(self): def test_check_constituenta_error(self):
self.owned.insert_new('X1') self.owned.insert_last('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM} data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
response = self.executeOK(data=data, item=self.owned_id) response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], False) self.assertEqual(response.data['parseResult'], False)
@ -149,7 +149,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/resolve', method='post') @decl_endpoint('/api/rsforms/{item}/resolve', method='post')
def test_resolve(self): def test_resolve(self):
x1 = self.owned.insert_new( x1 = self.owned.insert_last(
alias='X1', alias='X1',
term_resolved='синий слон' term_resolved='синий слон'
) )
@ -191,7 +191,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/export-trs', method='get') @decl_endpoint('/api/rsforms/{item}/export-trs', method='get')
def test_export_trs(self): def test_export_trs(self):
schema = RSForm.create(title='Test') schema = RSForm.create(title='Test')
schema.insert_new('X1') schema.insert_last('X1')
response = self.executeOK(item=schema.model.pk) response = self.executeOK(item=schema.model.pk)
self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs') self.assertEqual(response.headers['Content-Disposition'], 'attachment; filename=Schema.trs')
with io.BytesIO(response.content) as stream: with io.BytesIO(response.content) as stream:
@ -206,8 +206,8 @@ class TestRSFormViewset(EndpointTester):
self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.unowned_id)
data = {'alias': 'X3'} data = {'alias': 'X3'}
self.owned.insert_new('X1') self.owned.insert_last('X1')
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id) self.executeBadData(data=data, item=self.owned_id)
@ -225,7 +225,9 @@ class TestRSFormViewset(EndpointTester):
'cst_type': CstType.BASE, 'cst_type': CstType.BASE,
'insert_after': x2.pk, 'insert_after': x2.pk,
'term_raw': 'test', 'term_raw': 'test',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}] 'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
'definition_formal': 'invalid',
'crucial': True
} }
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], data['alias']) self.assertEqual(response.data['new_cst']['alias'], data['alias'])
@ -233,6 +235,8 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x4.order, 2) self.assertEqual(x4.order, 2)
self.assertEqual(x4.term_raw, data['term_raw']) self.assertEqual(x4.term_raw, data['term_raw'])
self.assertEqual(x4.term_forms, data['term_forms']) self.assertEqual(x4.term_forms, data['term_forms'])
self.assertEqual(x4.definition_formal, data['definition_formal'])
self.assertEqual(x4.crucial, data['crucial'])
data = { data = {
'alias': 'X5', 'alias': 'X5',
@ -247,11 +251,11 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch') @decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_multiple(self): def test_substitute_multiple(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
d1 = self.owned.insert_new('D1') d1 = self.owned.insert_last('D1')
d2 = self.owned.insert_new('D2') d2 = self.owned.insert_last('D2')
d3 = self.owned.insert_new( d3 = self.owned.insert_last(
alias='D3', alias='D3',
definition_formal=r'X1 \ X2' definition_formal=r'X1 \ X2'
) )
@ -314,19 +318,19 @@ class TestRSFormViewset(EndpointTester):
data = {'items': [1337]} data = {'items': [1337]}
self.executeBadData(data=data) self.executeBadData(data=data)
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
data = {'items': [x1.pk]} data = {'items': [x1.pk]}
response = self.executeOK(data=data) response = self.executeOK(data=data)
x2.refresh_from_db() x2.refresh_from_db()
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.assertEqual(len(response.data['items']), 1) self.assertEqual(len(response.data['items']), 1)
self.assertEqual(self.owned.constituents().count(), 1) self.assertEqual(self.owned.constituentsQ().count(), 1)
self.assertEqual(x2.alias, 'X2') self.assertEqual(x2.alias, 'X2')
self.assertEqual(x2.order, 0) self.assertEqual(x2.order, 0)
x3 = self.unowned.insert_new('X1') x3 = self.unowned.insert_last('X1')
data = {'items': [x3.pk]} data = {'items': [x3.pk]}
self.executeBadData(data=data, item=self.owned_id) self.executeBadData(data=data, item=self.owned_id)
@ -338,8 +342,8 @@ class TestRSFormViewset(EndpointTester):
data = {'items': [1337], 'move_to': 0} data = {'items': [1337], 'move_to': 0}
self.executeBadData(data=data) self.executeBadData(data=data)
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
data = {'items': [x2.pk], 'move_to': 0} data = {'items': [x2.pk], 'move_to': 0}
response = self.executeOK(data=data) response = self.executeOK(data=data)
@ -349,7 +353,7 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(x1.order, 1) self.assertEqual(x1.order, 1)
self.assertEqual(x2.order, 0) self.assertEqual(x2.order, 0)
x3 = self.unowned.insert_new('X1') x3 = self.unowned.insert_last('X1')
data = {'items': [x3.pk], 'move_to': 0} data = {'items': [x3.pk], 'move_to': 0}
self.executeBadData(data=data) self.executeBadData(data=data)
@ -361,9 +365,9 @@ class TestRSFormViewset(EndpointTester):
response = self.executeOK() response = self.executeOK()
self.assertEqual(response.data['id'], self.owned_id) self.assertEqual(response.data['id'], self.owned_id)
x2 = self.owned.insert_new('X2') x2 = self.owned.insert_last('X2')
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
d11 = self.owned.insert_new('D11') d11 = self.owned.insert_last('D11')
response = self.executeOK() response = self.executeOK()
x1.refresh_from_db() x1.refresh_from_db()
@ -383,41 +387,41 @@ class TestRSFormViewset(EndpointTester):
def test_load_trs(self): def test_load_trs(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
self.owned.model.title = 'Test11' self.owned.model.title = 'Test11'
self.owned.save() self.owned.model.save()
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
work_dir = os.path.dirname(os.path.abspath(__file__)) work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file: with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False} data = {'file': file, 'load_metadata': False}
response = self.client.patch(self.endpoint, data=data, format='multipart') response = self.client.patch(self.endpoint, data=data, format='multipart')
self.owned.refresh_from_db() self.owned.model.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.owned.model.title, 'Test11') self.assertEqual(self.owned.model.title, 'Test11')
self.assertEqual(len(response.data['items']), 25) self.assertEqual(len(response.data['items']), 25)
self.assertEqual(self.owned.constituents().count(), 25) self.assertEqual(self.owned.constituentsQ().count(), 25)
self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists()) self.assertFalse(Constituenta.objects.filter(pk=x1.pk).exists())
@decl_endpoint('/api/rsforms/{item}/produce-structure', method='patch') @decl_endpoint('/api/rsforms/{item}/produce-structure', method='patch')
def test_produce_structure(self): def test_produce_structure(self):
self.set_params(item=self.owned_id) self.set_params(item=self.owned_id)
x1 = self.owned.insert_new('X1') x1 = self.owned.insert_last('X1')
s1 = self.owned.insert_new( s1 = self.owned.insert_last(
alias='S1', alias='S1',
definition_formal='(X1×X1)' definition_formal='(X1×X1)'
) )
s2 = self.owned.insert_new( s2 = self.owned.insert_last(
alias='S2', alias='S2',
definition_formal='invalid' definition_formal='invalid'
) )
s3 = self.owned.insert_new( s3 = self.owned.insert_last(
alias='S3', alias='S3',
definition_formal='X1×(X1×(X1))×(X1×X1)' definition_formal='X1×(X1×(X1))×(X1×X1)'
) )
a1 = self.owned.insert_new( a1 = self.owned.insert_last(
alias='A1', alias='A1',
definition_formal='1=1' definition_formal='1=1'
) )
f1 = self.owned.insert_new( f1 = self.owned.insert_last(
alias='F10', alias='F10',
definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)' definition_formal='[α∈X1, β∈X1] Fi1[{α,β}](S1)'
) )
@ -511,7 +515,7 @@ class TestConstituentaAPI(EndpointTester):
data = {'target': self.cst1.pk, 'item_data': {'alias': self.cst3.alias}} data = {'target': self.cst1.pk, 'item_data': {'alias': self.cst3.alias}}
self.executeBadData(data=data, schema=self.owned_id) self.executeBadData(data=data, schema=self.owned_id)
d1 = self.owned.insert_new( d1 = self.owned.insert_last(
alias='D1', alias='D1',
term_raw='@{X1|plur}', term_raw='@{X1|plur}',
definition_formal='X1' definition_formal='X1'
@ -574,6 +578,19 @@ class TestConstituentaAPI(EndpointTester):
self.assertEqual(self.cst3.definition_resolved, 'form1') self.assertEqual(self.cst3.definition_resolved, 'form1')
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms']) 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): class TestInlineSynthesis(EndpointTester):
''' Testing Operations endpoints. ''' ''' Testing Operations endpoints. '''
@ -612,15 +629,15 @@ class TestInlineSynthesis(EndpointTester):
def test_inline_synthesis(self): def test_inline_synthesis(self):
ks1_x1 = self.schema1.insert_new('X1', term_raw='KS1X1') # -> delete ks1_x1 = self.schema1.insert_last('X1', term_raw='KS1X1') # -> delete
ks1_x2 = self.schema1.insert_new('X2', term_raw='KS1X2') # -> X2 ks1_x2 = self.schema1.insert_last('X2', term_raw='KS1X2') # -> X2
ks1_s1 = self.schema1.insert_new('S1', definition_formal='X2', term_raw='KS1S1') # -> S1 ks1_s1 = self.schema1.insert_last('S1', definition_formal='X2', term_raw='KS1S1') # -> S1
ks1_d1 = self.schema1.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D1 ks1_d1 = self.schema1.insert_last('D1', definition_formal=r'S1\X1\X2') # -> D1
ks2_x1 = self.schema2.insert_new('X1', term_raw='KS2X1') # -> delete ks2_x1 = self.schema2.insert_last('X1', term_raw='KS2X1') # -> delete
ks2_x2 = self.schema2.insert_new('X2', term_raw='KS2X2') # -> X4 ks2_x2 = self.schema2.insert_last('X2', term_raw='KS2X2') # -> X4
ks2_s1 = self.schema2.insert_new('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2 ks2_s1 = self.schema2.insert_last('S1', definition_formal='X2×X2', term_raw='KS2S1') # -> S2
ks2_d1 = self.schema2.insert_new('D1', definition_formal=r'S1\X1\X2') # -> D2 ks2_d1 = self.schema2.insert_last('D1', definition_formal=r'S1\X1\X2') # -> D2
ks2_a1 = self.schema2.insert_new('A1', definition_formal='1=1') # -> not included in items ks2_a1 = self.schema2.insert_last('A1', definition_formal='1=1') # -> not included in items
data = { data = {
'receiver': self.schema1.model.pk, 'receiver': self.schema1.model.pk,

View File

@ -42,6 +42,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'load_trs', 'load_trs',
'create_cst', 'create_cst',
'update_cst', 'update_cst',
'update_crucial',
'move_cst', 'move_cst',
'delete_multiple_cst', 'delete_multiple_cst',
'substitute', 'substitute',
@ -77,6 +78,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['post'], url_path='create-cst') @action(detail=True, methods=['post'], url_path='create-cst')
def create_cst(self, request: Request, pk) -> HttpResponse: def create_cst(self, request: Request, pk) -> HttpResponse:
''' Create Constituenta. ''' ''' Create Constituenta. '''
item = self._get_item()
serializer = s.CstCreateSerializer(data=request.data) serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data data = serializer.validated_data
@ -84,15 +86,18 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
insert_after = None insert_after = None
else: else:
insert_after = data['insert_after'] insert_after = data['insert_after']
schema = m.RSForm(self._get_item())
with transaction.atomic(): with transaction.atomic():
schema = m.RSFormCached(item)
new_cst = schema.create_cst(data, insert_after) new_cst = schema.create_cst(data, insert_after)
PropagationFacade.after_create_cst(schema, [new_cst]) PropagationFacade.after_create_cst(schema, [new_cst])
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_cst': s.CstInfoSerializer(new_cst).data, 'new_cst': s.CstInfoSerializer(new_cst).data,
'schema': s.RSFormParseSerializer(schema.model).data 'schema': s.RSFormParseSerializer(item).data
} }
) )
@ -110,15 +115,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='update-cst') @action(detail=True, methods=['patch'], url_path='update-cst')
def update_cst(self, request: Request, pk) -> HttpResponse: def update_cst(self, request: Request, pk) -> HttpResponse:
''' Update persistent attributes of a given constituenta. ''' ''' Update persistent attributes of a given constituenta. '''
model = self._get_item() item = self._get_item()
serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': model}) serializer = s.CstUpdateSerializer(data=request.data, partial=True, context={'schema': item})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) cst = cast(m.Constituenta, serializer.validated_data['target'])
schema = m.RSForm(model)
data = serializer.validated_data['item_data'] data = serializer.validated_data['item_data']
with transaction.atomic(): with transaction.atomic():
old_data = schema.update_cst(cst, data) schema = m.RSFormCached(item)
PropagationFacade.after_update_cst(schema, cst, data, old_data) old_data = schema.update_cst(cst.pk, data)
PropagationFacade.after_update_cst(schema, cst.pk, data, old_data)
if 'alias' in data and data['alias'] != cst.alias: if 'alias' in data and data['alias'] != cst.alias:
cst.refresh_from_db() cst.refresh_from_db()
changed_type = 'cst_type' in data and cst.cst_type != data['cst_type'] changed_type = 'cst_type' in data and cst.cst_type != data['cst_type']
@ -128,13 +134,42 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
cst.cst_type = data['cst_type'] cst.cst_type = data['cst_type']
cst.save() cst.save()
schema.apply_mapping(mapping=mapping, change_aliases=False) schema.apply_mapping(mapping=mapping, change_aliases=False)
schema.save()
cst.refresh_from_db()
if changed_type: if changed_type:
PropagationFacade.after_change_cst_type(schema, cst) PropagationFacade.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type))
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.model).data 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
) )
@extend_schema( @extend_schema(
@ -151,9 +186,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='produce-structure') @action(detail=True, methods=['patch'], url_path='produce-structure')
def produce_structure(self, request: Request, pk) -> HttpResponse: def produce_structure(self, request: Request, pk) -> HttpResponse:
''' Produce a term for every element of the target constituenta typification. ''' ''' Produce a term for every element of the target constituenta typification. '''
model = self._get_item() item = self._get_item()
serializer = s.CstTargetSerializer(data=request.data, context={'schema': model}) serializer = s.CstTargetSerializer(data=request.data, context={'schema': item})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst = cast(m.Constituenta, serializer.validated_data['target']) cst = cast(m.Constituenta, serializer.validated_data['target'])
if cst.cst_type not in [m.CstType.FUNCTION, m.CstType.STRUCTURED, m.CstType.TERM]: if cst.cst_type not in [m.CstType.FUNCTION, m.CstType.STRUCTURED, m.CstType.TERM]:
@ -161,27 +196,27 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
f'{cst.pk}': msg.constituentaNoStructure() f'{cst.pk}': msg.constituentaNoStructure()
}) })
schema_details = s.RSFormParseSerializer(model).data['items'] schema_details = s.RSFormParseSerializer(item).data['items']
cst_parse = next(item for item in schema_details if item['id'] == cst.pk)['parse'] cst_parse = next(item for item in schema_details if item['id'] == cst.pk)['parse']
if not cst_parse['typification']: if not cst_parse['typification']:
return Response( return Response(
status=c.HTTP_400_BAD_REQUEST, status=c.HTTP_400_BAD_REQUEST,
data={f'{cst.pk}': msg.constituentaNoStructure()} data={f'{cst.pk}': msg.constituentaNoStructure()}
) )
schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
schema = m.RSFormCached(item)
new_cst = schema.produce_structure(cst, cst_parse) new_cst = schema.produce_structure(cst, cst_parse)
PropagationFacade.after_create_cst(schema, new_cst) PropagationFacade.after_create_cst(schema, new_cst)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data={ data={
'cst_list': [cst.pk for cst in new_cst], 'cst_list': [cst.pk for cst in new_cst],
'schema': s.RSFormParseSerializer(schema.model).data 'schema': s.RSFormParseSerializer(item).data
} }
) )
@extend_schema( @extend_schema(
summary='execute substitutions', summary='execute substitutions',
tags=['RSForm'], tags=['RSForm'],
@ -196,24 +231,27 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='substitute') @action(detail=True, methods=['patch'], url_path='substitute')
def substitute(self, request: Request, pk) -> HttpResponse: def substitute(self, request: Request, pk) -> HttpResponse:
''' Substitute occurrences of constituenta with another one. ''' ''' Substitute occurrences of constituenta with another one. '''
model = self._get_item() item = self._get_item()
serializer = s.CstSubstituteSerializer( serializer = s.CstSubstituteSerializer(
data=request.data, data=request.data,
context={'schema': model} context={'schema': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema = m.RSForm(model)
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = [] substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
with transaction.atomic(): with transaction.atomic():
schema = m.RSForm(item)
for substitution in serializer.validated_data['substitutions']: for substitution in serializer.validated_data['substitutions']:
original = cast(m.Constituenta, substitution['original']) original = cast(m.Constituenta, substitution['original'])
replacement = cast(m.Constituenta, substitution['substitution']) replacement = cast(m.Constituenta, substitution['substitution'])
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(schema, substitutions) PropagationFacade.before_substitute(item.pk, substitutions)
schema.substitute(substitutions) schema.substitute(substitutions)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.model).data data=s.RSFormParseSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -230,20 +268,23 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='delete-multiple-cst') @action(detail=True, methods=['patch'], url_path='delete-multiple-cst')
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse: def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete multiple Constituents. ''' ''' Endpoint: Delete multiple Constituents. '''
model = self._get_item() item = self._get_item()
serializer = s.CstListSerializer( serializer = s.CstListSerializer(
data=request.data, data=request.data,
context={'schema': model} context={'schema': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cst_list: list[m.Constituenta] = serializer.validated_data['items'] cst_list: list[m.Constituenta] = serializer.validated_data['items']
schema = m.RSForm(model)
with transaction.atomic(): with transaction.atomic():
PropagationFacade.before_delete_cst(schema, cst_list) schema = m.RSForm(item)
PropagationFacade.before_delete_cst(item.pk, [cst.pk for cst in cst_list])
schema.delete_cst(cst_list) schema.delete_cst(cst_list)
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(schema.model).data data=s.RSFormParseSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -260,20 +301,24 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='move-cst') @action(detail=True, methods=['patch'], url_path='move-cst')
def move_cst(self, request: Request, pk) -> HttpResponse: def move_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Move multiple Constituents. ''' ''' Endpoint: Move multiple Constituents. '''
model = self._get_item() item = self._get_item()
serializer = s.CstMoveSerializer( serializer = s.CstMoveSerializer(
data=request.data, data=request.data,
context={'schema': model} context={'schema': item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
m.RSForm(model).move_cst( schema = m.RSForm(item)
schema.move_cst(
target=serializer.validated_data['items'], target=serializer.validated_data['items'],
destination=serializer.validated_data['move_to'] destination=serializer.validated_data['move_to']
) )
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -289,12 +334,16 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='reset-aliases') @action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request: Request, pk) -> HttpResponse: def reset_aliases(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Recreate all aliases based on order. ''' ''' Endpoint: Recreate all aliases based on order. '''
model = self._get_item() item = self._get_item()
schema = m.RSForm(model)
with transaction.atomic():
schema = m.RSForm(item)
schema.reset_aliases() schema.reset_aliases()
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -310,11 +359,15 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
@action(detail=True, methods=['patch'], url_path='restore-order') @action(detail=True, methods=['patch'], url_path='restore-order')
def restore_order(self, request: Request, pk) -> HttpResponse: def restore_order(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Restore order based on types and Term graph. ''' ''' Endpoint: Restore order based on types and Term graph. '''
model = self._get_item() item = self._get_item()
m.RSForm(model).restore_order()
with transaction.atomic():
m.OrderManager(m.RSFormCached(item)).restore_order()
item.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(model).data data=s.RSFormParseSerializer(item).data
) )
@extend_schema( @extend_schema(
@ -334,7 +387,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
input_serializer = s.RSFormUploadSerializer(data=request.data) input_serializer = s.RSFormUploadSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True) input_serializer.is_valid(raise_exception=True)
model = self._get_item() item = self._get_item()
load_metadata = input_serializer.validated_data['load_metadata'] load_metadata = input_serializer.validated_data['load_metadata']
data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) data = utility.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
if data is None: if data is None:
@ -342,7 +395,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
status=c.HTTP_400_BAD_REQUEST, status=c.HTTP_400_BAD_REQUEST,
data={'file': msg.exteorFileCorrupted()} data={'file': msg.exteorFileCorrupted()}
) )
data['id'] = model.pk data['id'] = item.pk
serializer = s.RSFormTRSSerializer( serializer = s.RSFormTRSSerializer(
data=data, data=data,
@ -406,7 +459,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.ExpressionSerializer(data=request.data) serializer = s.ExpressionSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
expression = serializer.validated_data['expression'] expression = serializer.validated_data['expression']
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object())) pySchema = s.PyConceptAdapter(pk)
result = pyconcept.check_expression(json.dumps(pySchema.data), expression) result = pyconcept.check_expression(json.dumps(pySchema.data), expression)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -431,7 +484,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
alias = serializer.validated_data['alias'] alias = serializer.validated_data['alias']
cst_type = cast(m.CstType, serializer.validated_data['cst_type']) cst_type = cast(m.CstType, serializer.validated_data['cst_type'])
pySchema = s.PyConceptAdapter(m.RSForm(self.get_object())) pySchema = s.PyConceptAdapter(pk)
result = pyconcept.check_constituenta(json.dumps(pySchema.data), alias, expression, cst_type) result = pyconcept.check_constituenta(json.dumps(pySchema.data), alias, expression, cst_type)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -453,7 +506,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
serializer = s.TextSerializer(data=request.data) serializer = s.TextSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
text = serializer.validated_data['text'] text = serializer.validated_data['text']
resolver = m.RSForm(self.get_object()).resolver() resolver = m.RSForm.resolver_from_schema(pk)
resolver.resolve(text) resolver.resolve(text)
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
@ -473,7 +526,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def export_trs(self, request: Request, pk) -> HttpResponse: def export_trs(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Download Exteor compatible file. ''' ''' Endpoint: Download Exteor compatible file. '''
model = self._get_item() model = self._get_item()
data = s.RSFormTRSSerializer(m.RSForm(model)).data data = s.generate_trs(model)
file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME) file = utility.write_zipped_json(data, utils.EXTEOR_INNER_FILENAME)
filename = utils.filename_for_schema(model.alias) filename = utils.filename_for_schema(model.alias)
response = HttpResponse(file, content_type='application/zip') response = HttpResponse(file, content_type='application/zip')
@ -593,10 +646,11 @@ def inline_synthesis(request: Request) -> HttpResponse:
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
receiver = m.RSForm(serializer.validated_data['receiver']) receiver = m.RSFormCached(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items']) items = cast(list[m.Constituenta], serializer.validated_data['items'])
if len(items) == 0: if len(items) == 0:
items = list(m.RSForm(serializer.validated_data['source']).constituents().order_by('order')) source = cast(LibraryItem, serializer.validated_data['source'])
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))
with transaction.atomic(): with transaction.atomic():
new_items = receiver.insert_copy(items) new_items = receiver.insert_copy(items)
@ -614,8 +668,9 @@ def inline_synthesis(request: Request) -> HttpResponse:
replacement = new_items[index] replacement = new_items[index]
substitutions.append((original, replacement)) substitutions.append((original, replacement))
PropagationFacade.before_substitute(receiver, substitutions) PropagationFacade.before_substitute(receiver.model.pk, substitutions)
receiver.substitute(substitutions) receiver.substitute(substitutions)
receiver.model.save(update_fields=['time_update'])
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,

View File

@ -4,8 +4,10 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
User = get_user_model() User = get_user_model()
admin.site.unregister(User)
@admin.register(User)
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
''' Admin model: User. ''' ''' Admin model: User. '''
fieldsets = UserAdmin.fieldsets fieldsets = UserAdmin.fieldsets
@ -21,7 +23,3 @@ class CustomUserAdmin(UserAdmin):
ordering = ['date_joined', 'username'] ordering = ['date_joined', 'username']
search_fields = ['email', 'first_name', 'last_name', 'username'] search_fields = ['email', 'first_name', 'last_name', 'username']
list_filter = ['is_staff', 'is_superuser', 'is_active'] list_filter = ['is_staff', 'is_superuser', 'is_active']
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)

View File

@ -86,10 +86,22 @@ def operationInputAlreadyConnected():
return 'Схема уже подключена к другой операции' return 'Схема уже подключена к другой операции'
def referenceTypeNotAllowed():
return 'Ссылки не поддерживаются'
def referenceTypeRequired():
return 'Операция должна быть ссылкой'
def operationNotSynthesis(title: str): def operationNotSynthesis(title: str):
return f'Операция не является Синтезом: {title}' return f'Операция не является Синтезом: {title}'
def operationResultEmpty(title: str):
return f'Результат операции пуст: {title}'
def operationResultNotEmpty(title: str): def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}' return f'Результат операции не пуст: {title}'
@ -130,10 +142,6 @@ def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии' return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'
def invalidPosition():
return 'Invalid position: should be positive integer'
def constituentaNoStructure(): def constituentaNoStructure():
return 'Указанная конституента не обладает теоретико-множественной типизацией' return 'Указанная конституента не обладает теоретико-множественной типизацией'

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "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", "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",
"test": "jest", "test": "jest",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"dev": "vite --host", "dev": "vite --host",
@ -15,30 +15,30 @@
}, },
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.5", "@dagrejs/dagre": "^1.1.5",
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.2.1",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@radix-ui/react-popover": "^1.1.14", "@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.81.5", "@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@uiw/codemirror-themes": "^4.24.0", "@uiw/codemirror-themes": "^4.24.1",
"@uiw/react-codemirror": "^4.24.0", "@uiw/react-codemirror": "^4.24.1",
"axios": "^1.10.0", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"global": "^4.4.0", "global": "^4.4.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"lucide-react": "^0.525.0", "lucide-react": "^0.533.0",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.0", "react": "^19.1.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.60.0", "react-hook-form": "^7.61.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-intl": "^7.1.11", "react-intl": "^7.1.11",
"react-router": "^7.6.3", "react-router": "^7.7.1",
"react-scan": "^0.4.3", "react-scan": "^0.4.3",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
@ -46,41 +46,41 @@
"react-zoom-pan-pinch": "^3.7.0", "react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.5", "tw-animate-css": "^1.3.6",
"use-debounce": "^10.0.5", "use-debounce": "^10.0.5",
"zod": "^3.25.76", "zod": "^4.0.13",
"zustand": "^5.0.6" "zustand": "^5.0.6"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.8.0", "@lezer/generator": "^1.8.0",
"@playwright/test": "^1.53.2", "@playwright/test": "^1.54.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.0.10", "@types/node": "^24.1.0",
"@types/react": "^19.1.8", "@types/react": "^19.1.9",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.7.0",
"babel-plugin-react-compiler": "^19.1.0-rc.1", "babel-plugin-react-compiler": "^19.1.0-rc.1",
"eslint": "^9.30.1", "eslint": "^9.32.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-playwright": "^2.2.0", "eslint-plugin-playwright": "^2.2.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.1", "eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.3.0", "globals": "^16.3.0",
"jest": "^30.0.4", "jest": "^30.0.5",
"stylelint": "^16.21.1", "stylelint": "^16.23.0",
"stylelint-config-recommended": "^16.0.0", "stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0", "stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0", "stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7", "tailwindcss": "^4.0.7",
"ts-jest": "^29.4.0", "ts-jest": "^29.4.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.36.0", "typescript-eslint": "^8.38.0",
"vite": "^7.0.3" "vite": "^7.0.6"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",

View File

@ -3,6 +3,7 @@ import { Outlet } from 'react-router';
import clsx from 'clsx'; import clsx from 'clsx';
import { ModalLoader } from '@/components/modal'; import { ModalLoader } from '@/components/modal';
import { useBrowserNavigation } from '@/hooks/use-browser-navigation';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -16,10 +17,13 @@ import { MutationErrors } from './mutation-errors';
import { Navigation } from './navigation'; import { Navigation } from './navigation';
export function ApplicationLayout() { export function ApplicationLayout() {
useBrowserNavigation();
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const viewportHeight = useViewportHeight(); const viewportHeight = useViewportHeight();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation); const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const noNavigation = useAppLayoutStore(state => state.noNavigation); const noNavigation = useAppLayoutStore(state => state.noNavigation);
const toastBottom = useAppLayoutStore(state => state.toastBottom);
const noFooter = useAppLayoutStore(state => state.noFooter); const noFooter = useAppLayoutStore(state => state.noFooter);
const activeDialog = useDialogsStore(state => state.active); const activeDialog = useDialogsStore(state => state.active);
@ -32,6 +36,8 @@ export function ApplicationLayout() {
autoClose={3000} autoClose={3000}
draggable={false} draggable={false}
pauseOnFocusLoss={false} pauseOnFocusLoss={false}
position={toastBottom ? 'bottom-right' : 'top-right'}
newestOnTop={toastBottom}
/> />
<Suspense fallback={<ModalLoader />}> <Suspense fallback={<ModalLoader />}>

View File

@ -45,6 +45,11 @@ const DlgDeleteOperation = React.lazy(() =>
default: module.DlgDeleteOperation default: module.DlgDeleteOperation
})) }))
); );
const DlgDeleteReference = React.lazy(() =>
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
default: module.DlgDeleteReference
}))
);
const DlgEditEditors = React.lazy(() => const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({ import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
default: module.DlgEditEditors default: module.DlgEditEditors
@ -196,6 +201,8 @@ export const GlobalDialogs = () => {
return <DlgCreateVersion />; return <DlgCreateVersion />;
case DialogType.DELETE_OPERATION: case DialogType.DELETE_OPERATION:
return <DlgDeleteOperation />; return <DlgDeleteOperation />;
case DialogType.DELETE_REFERENCE:
return <DlgDeleteReference />;
case DialogType.GRAPH_PARAMETERS: case DialogType.GRAPH_PARAMETERS:
return <DlgGraphParams />; return <DlgGraphParams />;
case DialogType.RELOCATE_CONSTITUENTS: case DialogType.RELOCATE_CONSTITUENTS:

View File

@ -3,13 +3,21 @@ import { useDebounce } from 'use-debounce';
import { Loader } from '@/components/loader'; import { Loader } from '@/components/loader';
import { ModalBackdrop } from '@/components/modal/modal-backdrop'; 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'; import { PARAMETER } from '@/utils/constants';
export function GlobalLoader() { export function GlobalLoader() {
const navigation = useNavigation(); const navigation = useNavigation();
const isLoading = navigation.state === 'loading'; const isTransitioning = useTransitionTracker();
const [loadingDebounced] = useDebounce(isLoading, PARAMETER.navigationPopupDelay); const isManualNav = useAppTransitionStore(state => state.isNavigating);
const isRouterLoading = navigation.state === 'loading';
const [loadingDebounced] = useDebounce(
isRouterLoading || isTransitioning || isManualNav,
PARAMETER.navigationPopupDelay
);
if (!loadingDebounced) { if (!loadingDebounced) {
return null; return null;

View File

@ -0,0 +1,83 @@
'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>
);
}

View File

@ -1,3 +1,5 @@
import { globalIDs } from '@/utils/constants';
import { type Button } from '../props'; import { type Button } from '../props';
import { cn } from '../utils'; import { cn } from '../utils';
@ -15,7 +17,17 @@ interface SubmitButtonProps extends Button {
/** /**
* Displays submit type button with icon and text. * Displays submit type button with icon and text.
*/ */
export function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) { export function SubmitButton({
text = 'ОК',
icon,
title,
titleHtml,
hideTitle,
disabled,
loading,
className,
...restProps
}: SubmitButtonProps) {
return ( return (
<button <button
type='submit' type='submit'
@ -28,6 +40,10 @@ export function SubmitButton({ text = 'ОК', icon, disabled, loading, className
loading && 'cursor-progress', loading && 'cursor-progress',
className className
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}
data-tooltip-html={titleHtml}
data-tooltip-content={title}
data-tooltip-hidden={hideTitle}
disabled={disabled || loading} disabled={disabled || loading}
{...restProps} {...restProps}
> >

View File

@ -0,0 +1,37 @@
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>
);
}

View File

@ -30,7 +30,7 @@ export function TextURL({ text, href, title, color = 'text-primary', onClick }:
); );
} else if (onClick) { } else if (onClick) {
return ( return (
<button type='button' tabIndex={-1} className={design} onClick={onClick}> <button type='button' tabIndex={-1} className={design} title={title} onClick={onClick}>
{text} {text}
</button> </button>
); );

View File

@ -61,6 +61,7 @@ export { BiLastPage as IconPageLast } from 'react-icons/bi';
export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb'; export { TbCalendarPlus as IconDateCreate } from 'react-icons/tb';
export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb'; export { TbCalendarRepeat as IconDateUpdate } from 'react-icons/tb';
export { PiFileCsv as IconCSV } from 'react-icons/pi'; export { PiFileCsv as IconCSV } from 'react-icons/pi';
export { BsFiletypeJson as IconJSON } from 'react-icons/bs';
// ==== User status ======= // ==== User status =======
export { LuCircleUserRound as IconUser } from 'react-icons/lu'; export { LuCircleUserRound as IconUser } from 'react-icons/lu';
@ -82,7 +83,7 @@ export { TbGps as IconCoordinates } from 'react-icons/tb';
export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { TbHexagons as IconOSS } from 'react-icons/tb'; export { TbHexagons as IconOSS } from 'react-icons/tb';
export { BiScreenshot as IconConceptBlock } from 'react-icons/bi'; export { MdOutlineSelectAll as IconConceptBlock } from 'react-icons/md';
export { TbHexagon as IconRSForm } from 'react-icons/tb'; export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb'; export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb'; export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
@ -94,6 +95,8 @@ export { TbHexagonLetterD as IconCstTerm } from 'react-icons/tb';
export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb'; export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb';
export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb'; export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb';
export { TbHexagonLetterT as IconCstTheorem } 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 { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu'; export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6'; export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
@ -107,6 +110,7 @@ export { LuPlaneTakeoff as IconRESTapi } from 'react-icons/lu';
export { LuImage as IconImage } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu';
export { GoVersions as IconVersions } from 'react-icons/go'; export { GoVersions as IconVersions } from 'react-icons/go';
export { LuAtSign as IconTerm } from 'react-icons/lu'; 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 { LuSubscript as IconAlias } from 'react-icons/lu';
export { TbMathFunction as IconFormula } from 'react-icons/tb'; export { TbMathFunction as IconFormula } from 'react-icons/tb';
export { BiFontFamily as IconText } from 'react-icons/bi'; export { BiFontFamily as IconText } from 'react-icons/bi';
@ -141,16 +145,17 @@ export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu'; export { LuReplace as IconReplace } from 'react-icons/lu';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa'; export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; 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 { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu'; export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr'; export { GrConnect as IconConnect } from 'react-icons/gr';
export { BiPlayCircle as IconExecute } from 'react-icons/bi'; export { BiPlayCircle as IconExecute } from 'react-icons/bi';
// ======== Graph UI ======= // ======== 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 { BiCollapse as IconGraphCollapse } from 'react-icons/bi';
export { BiExpand as IconGraphExpand } from 'react-icons/bi'; export { BiExpand as IconGraphExpand } from 'react-icons/bi';
export { LuMaximize as IconGraphMaximize } from 'react-icons/lu'; export { TiArrowMaximise as IconGraphMaximize } from 'react-icons/ti';
export { BiGitBranch as IconGraphInputs } from 'react-icons/bi'; export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { TbEarScan as IconGraphInverse } from 'react-icons/tb'; export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi'; export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';

View File

@ -21,6 +21,7 @@ export function DescribeError({ error }: { error: ErrorData }) {
return ( return (
<div> <div>
<p>Ошибка валидации данных</p> <p>Ошибка валидации данных</p>
{/* eslint-disable-next-line @typescript-eslint/no-base-to-string */}
<PrettyJson data={JSON.parse(error.toString()) as unknown} />; <PrettyJson data={JSON.parse(error.toString()) as unknown} />;
</div> </div>
); );

View File

@ -0,0 +1,24 @@
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)
}))
};
};
}

View File

@ -0,0 +1 @@
export { PromptInput } from './prompt-input';

View File

@ -0,0 +1,66 @@
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
}
);
}

View File

@ -0,0 +1,45 @@
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
});

View File

@ -0,0 +1,6 @@
import { styleTags, tags } from '@lezer/highlight';
export const highlighting = styleTags({
Variable: tags.name,
Error: tags.comment
});

View File

@ -0,0 +1,8 @@
import { LRLanguage } from '@codemirror/language';
import { parser } from './parser';
export const PromptLanguage = LRLanguage.define({
parser: parser,
languageData: {}
});

View File

@ -0,0 +1,5 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const Text = 1,
Variable = 2,
Error = 3,
Filler = 4;

View File

@ -0,0 +1,30 @@
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);
});
});

View File

@ -0,0 +1,20 @@
// 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
});

View File

@ -0,0 +1,39 @@
@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"

View File

@ -0,0 +1,144 @@
'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>
);
}
);

View File

@ -0,0 +1,87 @@
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 };
}

View File

@ -0,0 +1,81 @@
'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>
);
}

View File

@ -1,38 +1,26 @@
import { Suspense, useState } from 'react'; import { Suspense, useState } from 'react';
import { HelpTopic } from '@/features/help';
import { ComboBox } from '@/components/input/combo-box'; import { ComboBox } from '@/components/input/combo-box';
import { Loader } from '@/components/loader'; import { Loader } from '@/components/loader';
import { ModalView } from '@/components/modal'; import { ModalView } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates'; import { useAvailableTemplatesSuspense } from '../../backend/use-available-templates';
import { TabPromptResult } from './tab-prompt-result'; import { AIPromptTabs, TabID } from './ai-prompt-tabs';
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() { export function DlgAIPromptDialog() {
const [activeTab, setActiveTab] = useState<TabID>(TabID.SELECT); const [activeTab, setActiveTab] = useState<number>(TabID.TEMPLATE);
const [selected, setSelected] = useState<number | null>(null); const [selected, setSelected] = useState<number | null>(null);
const { items: prompts } = useAvailableTemplatesSuspense(); const { items: prompts } = useAvailableTemplatesSuspense();
return ( return (
<ModalView header='Генератор запросом LLM' className='w-100 sm:w-160 px-6'> <ModalView
<Tabs selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}> header='Генератор запросом LLM'
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'> className='w-100 sm:w-160 px-6 flex flex-col h-120'
<TabLabel label='Шаблон' /> helpTopic={HelpTopic.ASSISTANT}
<TabLabel label='Результат' disabled={!selected} /> >
<TabLabel label='Переменные' disabled={!selected} />
</TabList>
<div className='h-120 flex flex-col gap-2'>
<ComboBox <ComboBox
id='prompt-select' id='prompt-select'
items={prompts} items={prompts}
@ -45,12 +33,8 @@ export function DlgAIPromptDialog() {
className='w-full' className='w-full'
/> />
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<TabPanel>{selected ? <TabPromptSelect promptID={selected} /> : null}</TabPanel> {selected ? <AIPromptTabs promptID={selected} activeTab={activeTab} setActiveTab={setActiveTab} /> : null}
<TabPanel>{selected ? <TabPromptResult promptID={selected} /> : null}</TabPanel>
<TabPanel>{selected ? <TabPromptVariables promptID={selected} /> : null}</TabPanel>
</Suspense> </Suspense>
</div>
</Tabs>
</ModalView> </ModalView>
); );
} }

View File

@ -0,0 +1,54 @@
'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>
);
}

View File

@ -0,0 +1,38 @@
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>
);
}

View File

@ -1,65 +1,17 @@
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 { 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 { interface TabPromptResultProps {
promptID: number; prompt: string;
}
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);
} }
export function TabPromptResult({ prompt }: TabPromptResultProps) {
return ( 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 <TextArea
aria-label='Сгенерированное сообщение' aria-label='Сгенерированное сообщение'
value={generatedMessage} value={prompt}
placeholder='Текст шаблона пуст' placeholder='Текст шаблона пуст'
disabled disabled
fitContent className='w-full h-100'
className='w-full max-h-100 min-h-12'
/> />
</div>
); );
} }

View File

@ -1,44 +0,0 @@
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>
);
}

View File

@ -1,18 +1,16 @@
'use client'; 'use client';
import { usePromptTemplateSuspense } from '../../backend/use-prompt-template';
import { describePromptVariable } from '../../labels'; import { describePromptVariable } from '../../labels';
import { PromptVariableType } from '../../models/prompting'; import { PromptVariableType } from '../../models/prompting';
import { extractPromptVariables } from '../../models/prompting-api'; import { extractPromptVariables } from '../../models/prompting-api';
import { useAvailableVariables } from '../../stores/use-available-variables'; import { useAvailableVariables } from '../../stores/use-available-variables';
interface TabPromptVariablesProps { interface TabPromptVariablesProps {
promptID: number; template: string;
} }
export function TabPromptVariables({ promptID }: TabPromptVariablesProps) { export function TabPromptVariables({ template }: TabPromptVariablesProps) {
const { promptTemplate } = usePromptTemplateSuspense(promptID); const variables = extractPromptVariables(template);
const variables = extractPromptVariables(promptTemplate.text);
const availableTypes = useAvailableVariables(); const availableTypes = useAvailableVariables();
return ( return (
<ul> <ul>

View File

@ -2,6 +2,7 @@ import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { HelpTopic } from '@/features/help';
import { Checkbox, TextArea, TextInput } from '@/components/input'; import { Checkbox, TextArea, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal'; import { ModalForm } from '@/components/modal';
@ -52,6 +53,7 @@ export function DlgCreatePromptTemplate() {
onSubmit={event => void handleSubmit(onSubmit)(event)} onSubmit={event => void handleSubmit(onSubmit)(event)}
submitInvalidTooltip='Введите уникальное название шаблона' submitInvalidTooltip='Введите уникальное название шаблона'
className='cc-column w-140 max-h-120 py-2 px-6' 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} /> <TextInput id='dlg_prompt_label' {...register('label')} label='Название шаблона' error={errors.label} />
<TextArea id='dlg_prompt_description' {...register('description')} label='Описание' error={errors.description} /> <TextArea id='dlg_prompt_description' {...register('description')} label='Описание' error={errors.description} />

View File

@ -4,14 +4,22 @@ const describePromptVariableRecord: Record<PromptVariableType, string> = {
[PromptVariableType.BLOCK]: 'Текущий блок операционной схемы', [PromptVariableType.BLOCK]: 'Текущий блок операционной схемы',
[PromptVariableType.OSS]: 'Текущая операционная схема', [PromptVariableType.OSS]: 'Текущая операционная схема',
[PromptVariableType.SCHEMA]: 'Текущая концептуальная схема', [PromptVariableType.SCHEMA]: 'Текущая концептуальная схема',
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента' [PromptVariableType.SCHEMA_THESAURUS]: 'Термины и определения текущей концептуальной схемы',
[PromptVariableType.SCHEMA_GRAPH]: 'Граф связей определений конституент',
[PromptVariableType.SCHEMA_TYPE_GRAPH]: 'Граф ступеней концептуальной схемы',
[PromptVariableType.CONSTITUENTA]: 'Текущая конституента',
[PromptVariableType.CONSTITUENTA_SYNTAX_TREE]: 'Синтаксическое дерево конституенты'
}; };
const mockPromptVariableRecord: Record<PromptVariableType, string> = { const mockPromptVariableRecord: Record<PromptVariableType, string> = {
[PromptVariableType.BLOCK]: 'Пример: Текущий блок операционной схемы', [PromptVariableType.BLOCK]: 'Пример: Текущий блок операционной схемы',
[PromptVariableType.OSS]: 'Пример: Текущая операционная схема', [PromptVariableType.OSS]: 'Пример: Текущая операционная схема',
[PromptVariableType.SCHEMA]: 'Пример: Текущая концептуальная схема', [PromptVariableType.SCHEMA]: 'Пример: Текущая концептуальная схема',
[PromptVariableType.CONSTITUENTA]: 'Пример: Текущая конституента' [PromptVariableType.SCHEMA_THESAURUS]: 'Пример\nТермин1 - Определение1\nТермин2 - Определение2',
[PromptVariableType.SCHEMA_GRAPH]: 'Пример: Граф связей определений конституент',
[PromptVariableType.SCHEMA_TYPE_GRAPH]: 'Пример: Граф ступеней концептуальной схемы',
[PromptVariableType.CONSTITUENTA]: 'Пример: Текущая конституента',
[PromptVariableType.CONSTITUENTA_SYNTAX_TREE]: 'Пример синтаксического дерева конституенты'
}; };
/** Retrieves description for {@link PromptVariableType}. */ /** Retrieves description for {@link PromptVariableType}. */

View File

@ -1,3 +1,11 @@
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'; import { mockPromptVariable } from '../labels';
/** Extracts a list of variables (as string[]) from a target string. /** Extracts a list of variables (as string[]) from a target string.
@ -27,3 +35,125 @@ export function generateSample(target: string): string {
} }
return result; 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;
}

View File

@ -1,29 +1,15 @@
/** Represents prompt variable type. */ /** Represents prompt variable type. */
export const PromptVariableType = { export const PromptVariableType = {
BLOCK: 'block', SCHEMA: 'schema',
// BLOCK_TITLE: 'block.title', SCHEMA_THESAURUS: 'schema.thesaurus',
// BLOCK_DESCRIPTION: 'block.description', SCHEMA_GRAPH: 'schema.graph',
// BLOCK_CONTENTS: 'block.contents', SCHEMA_TYPE_GRAPH: 'schema.type-graph',
CONSTITUENTA: 'constituenta',
CONSTITUENTA_SYNTAX_TREE: 'constituenta.ast',
OSS: 'oss', OSS: 'oss',
// OSS_CONTENTS: 'oss.contents',
// OSS_ALIAS: 'oss.alias',
// OSS_TITLE: 'oss.title',
// OSS_DESCRIPTION: 'oss.description',
SCHEMA: 'schema', BLOCK: 'block'
// 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; } as const;
export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType]; export type PromptVariableType = (typeof PromptVariableType)[keyof typeof PromptVariableType];

View File

@ -14,7 +14,7 @@ import { useModificationStore } from '@/stores/modification';
import { PromptTabID, TemplatesTabs } from './templates-tabs'; import { PromptTabID, TemplatesTabs } from './templates-tabs';
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
tab: z.preprocess(v => (v ? Number(v) : undefined), z.nativeEnum(PromptTabID).default(PromptTabID.LIST)), tab: z.preprocess(v => (v ? Number(v) : undefined), z.enum(PromptTabID).default(PromptTabID.LIST)),
active: z.preprocess(v => (v ? Number(v) : undefined), z.number().nullable().default(null)) active: z.preprocess(v => (v ? Number(v) : undefined), z.number().nullable().default(null))
}); });
@ -26,7 +26,7 @@ export function PromptTemplatesPage() {
active: query.get('active') active: query.get('active')
}); });
const { isModified } = useModificationStore(); const isModified = useModificationStore(state => state.isModified);
useBlockNavigation(isModified); useBlockNavigation(isModified);
return ( return (

View File

@ -7,6 +7,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx'; import clsx from 'clsx';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { PromptInput } from '@/features/ai/components/prompt-input';
import { useAuthSuspense } from '@/features/auth'; import { useAuthSuspense } from '@/features/auth';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
@ -88,6 +89,11 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
}); });
} }
function handleChangeText(newValue: string, onChange: (newValue: string) => void) {
setSampleResult(null);
onChange(newValue);
}
return ( return (
<form <form
id={globalIDs.prompt_editor} id={globalIDs.prompt_editor}
@ -108,15 +114,22 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
error={errors.description} error={errors.description}
disabled={isProcessing || !isMutable} disabled={isProcessing || !isMutable}
/> />
<TextArea
<Controller
control={control}
name='text'
render={({ field }) => (
<PromptInput
id='prompt_text' id='prompt_text'
label='Содержание' // label='Содержание'
fitContent placeholder='Пример: Предложи дополнение для КС {{schema}}'
className='disabled:min-h-9 max-h-64' className='disabled:min-h-9 max-h-64'
{...register('text')} value={field.value}
error={errors.text} onChange={newValue => handleChangeText(newValue, field.onChange)}
disabled={isProcessing || !isMutable} disabled={isProcessing || !isMutable}
/> />
)}
/>
<div className='flex justify-between'> <div className='flex justify-between'>
<Controller <Controller
name='is_shared' name='is_shared'
@ -137,6 +150,7 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
title='Сгенерировать пример запроса' title='Сгенерировать пример запроса'
icon={<IconSample size='1.25rem' className='icon-primary' />} icon={<IconSample size='1.25rem' className='icon-primary' />}
onClick={() => setSampleResult(!!sampleResult ? null : generateSample(text))} onClick={() => setSampleResult(!!sampleResult ? null : generateSample(text))}
disabled={!text}
/> />
</div> </div>

View File

@ -1,10 +1,14 @@
import { useFitHeight } from '@/stores/app-layout';
import { describePromptVariable } from '../../labels'; import { describePromptVariable } from '../../labels';
import { PromptVariableType } from '../../models/prompting'; import { PromptVariableType } from '../../models/prompting';
/** Displays all prompt variable types with their descriptions. */ /** Displays all prompt variable types with their descriptions. */
export function TabViewVariables() { export function TabViewVariables() {
const panelHeight = useFitHeight('3rem');
return ( return (
<div className='pt-8'> <div className='mt-10 cc-scroll-y min-h-40' style={{ maxHeight: panelHeight }}>
<ul className='space-y-1'> <ul className='space-y-1'>
{Object.values(PromptVariableType).map(variableType => ( {Object.values(PromptVariableType).map(variableType => (
<li key={variableType} className='flex flex-col'> <li key={variableType} className='flex flex-col'>

View File

@ -1,6 +1,8 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { urls, useConceptNavigation } from '@/app'; import { urls, useConceptNavigation } from '@/app';
import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components/badge-help';
import { Loader } from '@/components/loader'; import { Loader } from '@/components/loader';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
@ -59,6 +61,9 @@ export function TemplatesTabs({ activeID, tab }: TemplatesTabsProps) {
<TabLabel label='Список' /> <TabLabel label='Список' />
<TabLabel label='Шаблон' /> <TabLabel label='Шаблон' />
<TabLabel label='Переменные' /> <TabLabel label='Переменные' />
<div className='flex px-1'>
<BadgeHelp topic={HelpTopic.ASSISTANT} offset={5} />
</div>
</TabList> </TabList>
<div className='overflow-x-hidden'> <div className='overflow-x-hidden'>
<TabPanel> <TabPanel>

View File

@ -1,10 +1,19 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { type IBlock, type IOperationSchema } from '@/features/oss/models/oss'; import { type IBlock, type IOperation, type IOperationSchema } from '@/features/oss/models/oss';
import { type IConstituenta, type IRSForm } from '@/features/rsform'; import { type IConstituenta, type IRSForm } from '@/features/rsform';
import { labelCstTypification } from '@/features/rsform/labels';
import { PromptVariableType } from '../models/prompting'; import { PromptVariableType } from '../models/prompting';
import {
varBlock,
varConstituenta,
varOSS,
varSchema,
varSchemaGraph,
varSchemaThesaurus,
varSchemaTypeGraph,
varSyntaxTree
} from '../models/prompting-api';
interface AIContextStore { interface AIContextStore {
currentOSS: IOperationSchema | null; currentOSS: IOperationSchema | null;
@ -16,6 +25,9 @@ interface AIContextStore {
currentBlock: IBlock | null; currentBlock: IBlock | null;
setCurrentBlock: (value: IBlock | null) => void; setCurrentBlock: (value: IBlock | null) => void;
currentOperation: IOperation | null;
setCurrentOperation: (value: IOperation | null) => void;
currentConstituenta: IConstituenta | null; currentConstituenta: IConstituenta | null;
setCurrentConstituenta: (value: IConstituenta | null) => void; setCurrentConstituenta: (value: IConstituenta | null) => void;
} }
@ -30,6 +42,9 @@ export const useAIStore = create<AIContextStore>()(set => ({
currentBlock: null, currentBlock: null,
setCurrentBlock: value => set({ currentBlock: value }), setCurrentBlock: value => set({ currentBlock: value }),
currentOperation: null,
setCurrentOperation: value => set({ currentOperation: value }),
currentConstituenta: null, currentConstituenta: null,
setCurrentConstituenta: value => set({ currentConstituenta: value }) setCurrentConstituenta: value => set({ currentConstituenta: value })
})); }));
@ -40,10 +55,12 @@ export function makeVariableSelector(variableType: PromptVariableType) {
case PromptVariableType.OSS: case PromptVariableType.OSS:
return (state: AIContextStore) => ({ currentOSS: state.currentOSS }); return (state: AIContextStore) => ({ currentOSS: state.currentOSS });
case PromptVariableType.SCHEMA: case PromptVariableType.SCHEMA:
case PromptVariableType.SCHEMA_THESAURUS:
return (state: AIContextStore) => ({ currentSchema: state.currentSchema }); return (state: AIContextStore) => ({ currentSchema: state.currentSchema });
case PromptVariableType.BLOCK: case PromptVariableType.BLOCK:
return (state: AIContextStore) => ({ currentBlock: state.currentBlock }); return (state: AIContextStore) => ({ currentBlock: state.currentBlock, currentOSS: state.currentOSS });
case PromptVariableType.CONSTITUENTA: case PromptVariableType.CONSTITUENTA:
case PromptVariableType.CONSTITUENTA_SYNTAX_TREE:
return (state: AIContextStore) => ({ currentConstituenta: state.currentConstituenta }); return (state: AIContextStore) => ({ currentConstituenta: state.currentConstituenta });
default: default:
return () => ({}); return () => ({});
@ -54,25 +71,22 @@ export function makeVariableSelector(variableType: PromptVariableType) {
export function evaluatePromptVariable(variableType: PromptVariableType, context: Partial<AIContextStore>): string { export function evaluatePromptVariable(variableType: PromptVariableType, context: Partial<AIContextStore>): string {
switch (variableType) { switch (variableType) {
case PromptVariableType.OSS: case PromptVariableType.OSS:
return context.currentOSS?.title ?? ''; return context.currentOSS ? varOSS(context.currentOSS) : `!${variableType}!`;
case PromptVariableType.SCHEMA: case PromptVariableType.SCHEMA:
return context.currentSchema ? generateSchemaPrompt(context.currentSchema) : ''; 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}!`;
case PromptVariableType.BLOCK: case PromptVariableType.BLOCK:
return context.currentBlock?.title ?? ''; return context.currentBlock && context.currentOSS
? varBlock(context.currentBlock, context.currentOSS)
: `!${variableType}!`;
case PromptVariableType.CONSTITUENTA: case PromptVariableType.CONSTITUENTA:
return context.currentConstituenta?.alias ?? ''; return context.currentConstituenta ? varConstituenta(context.currentConstituenta) : `!${variableType}!`;
case PromptVariableType.CONSTITUENTA_SYNTAX_TREE:
return context.currentConstituenta ? varSyntaxTree(context.currentConstituenta) : `!${variableType}!`;
} }
} }
// ====== 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