Compare commits

..

No commits in common. "6b2268a76b096889a5c18beb5f3a457931d807e2" and "266fdf0c304ff43ff1a07ce2bde3ab1b184a5ce3" have entirely different histories.

308 changed files with 4810 additions and 8287 deletions

15
.vscode/settings.json vendored
View File

@ -177,16 +177,11 @@
"Upvote",
"Viewset",
"viewsets",
"vkvideo",
"wordform",
"Wordforms",
"XCSDATN",
"Айзенштат",
"Акименков",
"Астрина",
"Атрибутирование",
"Атрибутирующая",
"Атрибутирующие",
"Ашихмин",
"Биективная",
"биективной",
@ -196,8 +191,6 @@
"Бурбакизатор",
"Версионирование",
"Владельцом",
"генемные",
"дебуль",
"Демешко",
"Десинглетон",
"доксинг",
@ -219,16 +212,9 @@
"Кучкаров",
"Кучкарова",
"мультиграфа",
"мультииндекс",
"Мультииндексы",
"Мультифильтр",
"неинтерпретируемый",
"неитерируемого",
"Никанорова",
"Номиноид",
"номиноида",
"номиноидом",
"Номиноиды",
"операционализации",
"операционализированных",
"Оргтеор",
@ -238,7 +224,6 @@
"подпапках",
"Присакарь",
"ПРОКСИМА",
"родовидовое",
"Родоструктурная",
"родоструктурного",
"Родоструктурное",

View File

@ -1,29 +1,33 @@
!! This is not complete list of TODOs !!
For more specific TODOs see comments in code
[Bugs - PENDING]
-
[Functionality - PENDING]
- Export PDF (Items list, Graph)
- Save react-flow to vector image
- Landing page
- Design first user experience
- Video guides
- Demo sandbox for anonymous users
- Implement rslang and rsmodel functionality in the frontend
- Allow manual setup for typification and value class
- Save react-flow to vector image
User profile:
- Settings server persistency
- Profile pictures (avatars)
- Profile pictures
- Custom LibraryItem lists
- Custom user filters and sharing filters
- Personal prompt templates
- Static analyzer for RSForm as a whole: check term duplication and empty conventions
- OSS clone and versioning
- Clone with saving info connection
- Semantic diff for library items
- Focus on codemirror editor when label is clicked (need React 19 ref for clean code solution)
- Draggable rows in constituents table
- Search functionality for Help Manuals - use google search integration filtered by site?
- Export PDF (Items list, Graph)
- ARIA (accessibility considerations) - for now machine reading not supported
- Internationalization - at least english version. Consider react.intl
- Sitemap for better SEO and crawler optimization

View File

@ -9,7 +9,7 @@ from apps.library.models import (
LibraryTemplate,
LocationHead
)
from apps.rsform.models import Attribution, RSForm
from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
@ -43,7 +43,7 @@ class TestLibraryViewset(EndpointTester):
'title': 'Title',
'alias': 'alias',
}
response = self.executeCreated(data)
response = self.executeCreated(data=data)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['item_type'], LibraryItemType.RSFORM)
self.assertEqual(response.data['title'], data['title'])
@ -57,7 +57,7 @@ class TestLibraryViewset(EndpointTester):
'visible': False,
'read_only': True
}
response = self.executeCreated(data)
response = self.executeCreated(data=data)
oss = LibraryItem.objects.get(pk=response.data['id'])
self.assertEqual(oss.owner, self.user)
self.assertEqual(response.data['owner'], self.user.pk)
@ -70,25 +70,25 @@ class TestLibraryViewset(EndpointTester):
self.logout()
data = {'title': 'Title2'}
self.executeForbidden(data)
self.executeForbidden(data=data)
@decl_endpoint('/api/library/{item}', method='patch')
def test_update(self):
data = {'title': 'New Title'}
self.executeNotFound(data, item=self.invalid_item)
self.executeForbidden(data, item=self.unowned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.toggle_editor(self.unowned, True)
response = self.executeOK(data, item=self.unowned.pk)
response = self.executeOK(data=data, item=self.unowned.pk)
self.assertEqual(response.data['title'], data['title'])
self.unowned.access_policy = AccessPolicy.PRIVATE
self.unowned.save()
self.executeForbidden(data, item=self.unowned.pk)
self.executeForbidden(data=data, item=self.unowned.pk)
data = {'title': 'New Title'}
response = self.executeOK(data, item=self.owned.pk)
response = self.executeOK(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], self.owned.alias)
@ -98,7 +98,7 @@ class TestLibraryViewset(EndpointTester):
'access_policy': AccessPolicy.PROTECTED,
'location': LocationHead.LIBRARY
}
response = self.executeOK(data, item=self.owned.pk)
response = self.executeOK(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['owner'], self.owned.owner.pk)
self.assertEqual(response.data['access_policy'], self.owned.access_policy)
@ -111,22 +111,22 @@ class TestLibraryViewset(EndpointTester):
time_update = self.owned.time_update
data = {'user': self.user.pk}
self.executeNotFound(data, item=self.invalid_item)
self.executeForbidden(data, item=self.unowned.pk)
self.executeOK(data, item=self.owned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.owner, self.user)
data = {'user': self.user2.pk}
self.executeOK(data, item=self.owned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.owner, self.user2)
self.assertEqual(self.owned.time_update, time_update)
self.executeForbidden(data, item=self.owned.pk)
self.executeForbidden(data=data, item=self.owned.pk)
self.toggle_admin(True)
data = {'user': self.user.pk}
self.executeOK(data, item=self.owned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.owner, self.user)
@ -135,20 +135,20 @@ class TestLibraryViewset(EndpointTester):
time_update = self.owned.time_update
data = {'access_policy': 'invalid'}
self.executeBadData(data, item=self.owned.pk)
self.executeBadData(data=data, item=self.owned.pk)
data = {'access_policy': AccessPolicy.PRIVATE}
self.executeNotFound(data, item=self.invalid_item)
self.executeForbidden(data, item=self.unowned.pk)
self.executeOK(data, item=self.owned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.access_policy, data['access_policy'])
self.toggle_editor(self.unowned, True)
self.executeForbidden(data, item=self.unowned.pk)
self.executeForbidden(data=data, item=self.unowned.pk)
self.toggle_admin(True)
self.executeOK(data, item=self.unowned.pk)
self.executeOK(data=data, item=self.unowned.pk)
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.access_policy, data['access_policy'])
@ -157,29 +157,29 @@ class TestLibraryViewset(EndpointTester):
time_update = self.owned.time_update
data = {'location': 'invalid'}
self.executeBadData(data, item=self.owned.pk)
self.executeBadData(data=data, item=self.owned.pk)
data = {'location': '/U/temp'}
self.executeNotFound(data, item=self.invalid_item)
self.executeForbidden(data, item=self.unowned.pk)
self.executeOK(data, item=self.owned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.location, data['location'])
data = {'location': LocationHead.LIBRARY}
self.executeForbidden(data, item=self.owned.pk)
self.executeForbidden(data=data, item=self.owned.pk)
data = {'location': '/U/temp'}
self.toggle_editor(self.unowned, True)
self.executeForbidden(data, item=self.unowned.pk)
self.executeForbidden(data=data, item=self.unowned.pk)
self.toggle_admin(True)
data = {'location': LocationHead.LIBRARY}
self.executeOK(data, item=self.owned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.location, data['location'])
self.executeOK(data, item=self.unowned.pk)
self.executeOK(data=data, item=self.unowned.pk)
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.location, data['location'])
@ -201,12 +201,12 @@ class TestLibraryViewset(EndpointTester):
'new_location': '/S/temp2'
}
self.executeBadData({})
self.executeBadData({'target:': '/S/temp'})
self.executeBadData({'new_location:': '/S/temp'})
self.executeBadData({'target:': 'invalid', 'new_location': '/S/temp'})
self.executeBadData({'target:': '/S/temp', 'new_location': 'invalid'})
self.executeOK(data)
self.executeBadData(data={})
self.executeBadData(data={'target:': '/S/temp'})
self.executeBadData(data={'new_location:': '/S/temp'})
self.executeBadData(data={'target:': 'invalid', 'new_location': '/S/temp'})
self.executeBadData(data={'target:': '/S/temp', 'new_location': 'invalid'})
self.executeOK(data=data)
self.owned.refresh_from_db()
self.unowned.refresh_from_db()
owned2.refresh_from_db()
@ -215,7 +215,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(owned2.location, '/S/temp2/123')
self.toggle_admin(True)
self.executeOK(data)
self.executeOK(data=data)
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.location, '/S/temp2')
@ -232,7 +232,7 @@ class TestLibraryViewset(EndpointTester):
}
self.toggle_admin(True)
self.executeOK(data)
self.executeOK(data=data)
self.owned.refresh_from_db()
self.unowned.refresh_from_db()
self.assertEqual(self.owned.location, '/U/temp2')
@ -243,30 +243,30 @@ class TestLibraryViewset(EndpointTester):
time_update = self.owned.time_update
data = {'users': [self.invalid_user]}
self.executeBadData(data, item=self.owned.pk)
self.executeBadData(data=data, item=self.owned.pk)
data = {'users': [self.user.pk]}
self.executeNotFound(data, item=self.invalid_item)
self.executeForbidden(data, item=self.unowned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data, item=self.owned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.time_update, time_update)
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
self.executeOK(data)
self.executeOK(data=data)
self.assertEqual(list(self.owned.getQ_editors()), [self.user])
data = {'users': [self.user2.pk]}
self.executeOK(data)
self.executeOK(data=data)
self.assertEqual(list(self.owned.getQ_editors()), [self.user2])
data = {'users': []}
self.executeOK(data)
self.executeOK(data=data)
self.assertEqual(list(self.owned.getQ_editors()), [])
data = {'users': [self.user2.pk, self.user.pk]}
self.executeOK(data)
self.executeOK(data=data)
self.assertEqual(set(self.owned.getQ_editors()), set([self.user2, self.user]))
@ -343,16 +343,14 @@ class TestLibraryViewset(EndpointTester):
term_raw='@{X12|plur}',
term_resolved='люди'
)
Attribution.objects.create(container=d2, attribute=x12)
data = {'item_data': {'title': 'Title1337'}, 'items': []}
self.executeNotFound(data, item=self.invalid_item)
self.executeCreated(data, item=self.unowned.pk)
self.executeNotFound(data=data, item=self.invalid_item)
self.executeCreated(data=data, item=self.unowned.pk)
response = self.executeCreated(data, item=self.owned.pk)
response = self.executeCreated(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['item_data']['title'])
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(len(response.data['attribution']), 1)
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
@ -360,12 +358,12 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
data = {'item_data': {'title': 'Title1340'}, 'items': []}
response = self.executeCreated(data, item=self.owned.pk)
response = self.executeCreated(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['item_data']['title'])
self.assertEqual(len(response.data['items']), 2)
data = {'item_data': {'title': 'Title1341'}, 'items': [x12.pk]}
response = self.executeCreated(data, item=self.owned.pk)
response = self.executeCreated(data=data, item=self.owned.pk)
self.assertEqual(response.data['title'], data['item_data']['title'])
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(response.data['items'][0]['alias'], x12.alias)

View File

@ -32,11 +32,11 @@ class TestVersionViews(EndpointTester):
invalid_id = 1338
data = {'version': '1.0.0', 'description': 'test'}
self.executeNotFound(data, schema=invalid_id)
self.executeForbidden(data, schema=self.unowned_id)
self.executeBadData(invalid_data, schema=self.owned_id)
self.executeNotFound(data=data, schema=invalid_id)
self.executeForbidden(data=data, schema=self.unowned_id)
self.executeBadData(data=invalid_data, schema=self.owned_id)
response = self.executeCreated(data, schema=self.owned_id)
response = self.executeCreated(data=data, schema=self.owned_id)
self.assertTrue('version' in response.data)
self.assertTrue('schema' in response.data)
self.assertTrue(response.data['version'] in [v['id'] for v in response.data['schema']['versions']])
@ -46,7 +46,7 @@ class TestVersionViews(EndpointTester):
def test_create_version_filter(self):
x2 = self.owned.insert_last('X2')
data = {'version': '1.0.0', 'description': 'test', 'items': [x2.pk]}
response = self.executeCreated(data, schema=self.owned_id)
response = self.executeCreated(data=data, schema=self.owned_id)
version = Version.objects.get(pk=response.data['version'])
items = version.data['items']
self.assertTrue('version' in response.data)
@ -102,7 +102,7 @@ class TestVersionViews(EndpointTester):
@decl_endpoint('/api/versions/{version}', method='get')
def test_access_version(self):
data = {'version': '1.0.0', 'description': 'test'}
version_id = self._create_version(data)
version_id = self._create_version(data=data)
invalid_id = version_id + 1337
self.executeNotFound(version=invalid_id)
@ -116,14 +116,14 @@ class TestVersionViews(EndpointTester):
data = {'version': '1.2.0', 'description': 'test1'}
self.method = 'patch'
self.executeForbidden(data)
self.executeForbidden(data=data)
self.method = 'delete'
self.executeForbidden()
self.client.force_authenticate(user=self.user)
self.method = 'patch'
self.executeOK(data)
self.executeOK(data=data)
response = self.get()
self.assertEqual(response.data['version'], data['version'])
self.assertEqual(response.data['description'], data['description'])
@ -157,7 +157,7 @@ class TestVersionViews(EndpointTester):
x2 = self.owned.insert_last('X2')
d1 = self.owned.insert_last('D1', term_raw='TestTerm')
data = {'version': '1.0.0', 'description': 'test'}
version_id = self._create_version(data)
version_id = self._create_version(data=data)
invalid_id = version_id + 1337
Constituenta.objects.get(pk=d1.pk).delete()
@ -186,7 +186,7 @@ class TestVersionViews(EndpointTester):
def _create_version(self, data) -> int:
response = self.client.post(
f'/api/library/{self.owned_id}/create-version',
data, format='json'
data=data, format='json'
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
return response.data['version'] # type: ignore

View File

@ -14,7 +14,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
from apps.rsform.models import Attribution, RSFormCached
from apps.rsform.models import RSFormCached
from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User
from shared import permissions
@ -157,8 +157,8 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer = s.LibraryItemCloneSerializer(data=request.data, context={'schema': item})
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.pk = None
@ -171,24 +171,12 @@ class LibraryViewSet(viewsets.ModelViewSet):
clone.access_policy = data.get('access_policy', m.AccessPolicy.PUBLIC)
clone.location = data.get('location', m.LocationHead.USER)
clone.save()
cst_map: dict[int, int] = {}
cst_list: list[int] = []
need_filter = 'items' in request.data and request.data['items']
need_filter = 'items' in request.data and len(request.data['items']) > 0
for cst in RSFormCached(item).constituentsQ():
if not need_filter or cst.pk in request.data['items']:
old_pk = cst.pk
cst.pk = None
cst.schema = clone
cst.save()
cst_map[old_pk] = cst.pk
cst_list.append(old_pk)
for attr in Attribution.objects.filter(container__in=cst_list, attribute__in=cst_list):
attr.pk = None
attr.container_id = cst_map[attr.container_id]
attr.attribute_id = cst_map[attr.attribute_id]
attr.save()
return Response(
status=c.HTTP_201_CREATED,
data=RSFormParseSerializer(clone).data
@ -311,7 +299,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
with transaction.atomic():
added, deleted = m.Editor.set_and_return_diff(item.pk, editors)
if added or deleted:
if len(added) >= 0 or len(deleted) >= 0:
owned_schemas = OperationSchema.owned_schemasQ(item).only('pk')
if owned_schemas.exists():
m.Editor.objects.filter(

View File

@ -60,9 +60,9 @@ class InheritanceAdmin(admin.ModelAdmin):
search_fields = ['id', 'operation', 'parent', 'child']
@admin.register(models.Replica)
class ReplicaAdmin(admin.ModelAdmin):
''' Admin model: Replica. '''
ordering = ['replica', 'original']
list_display = ['id', 'replica', 'original']
search_fields = ['id', 'replica', 'original']
@admin.register(models.Reference)
class ReferenceAdmin(admin.ModelAdmin):
''' Admin model: Reference. '''
ordering = ['reference', 'target']
list_display = ['id', 'reference', 'target']
search_fields = ['id', 'reference', 'target']

View File

@ -1,35 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-06 09:10
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('oss', '0015_reference'),
]
operations = [
migrations.AlterField(
model_name='operation',
name='operation_type',
field=models.CharField(choices=[('input', 'Input'), ('synthesis', 'Synthesis'), ('replica', 'Replica')], default='input', max_length=10, verbose_name='Тип'),
),
migrations.CreateModel(
name='Replica',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('original', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targets', to='oss.operation', verbose_name='Целевая Операция')),
('replica', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replicas', to='oss.operation', verbose_name='Реплика')),
],
options={
'verbose_name': 'Реплика',
'verbose_name_plural': 'Реплики',
'unique_together': {('replica', 'original')},
},
),
migrations.DeleteModel(
name='Reference',
),
]

View File

@ -16,7 +16,7 @@ from django.db.models import (
from apps.library.models import LibraryItem
from .Argument import Argument
from .Replica import Replica
from .Reference import Reference
from .Substitution import Substitution
@ -24,7 +24,7 @@ class OperationType(TextChoices):
''' Type of operation. '''
INPUT = 'input'
SYNTHESIS = 'synthesis'
REPLICA = 'replica'
REFERENCE = 'reference'
class Operation(Model):
@ -93,13 +93,13 @@ class Operation(Model):
''' Operation substitutions. '''
return Substitution.objects.filter(operation=self)
def getQ_replicas(self) -> QuerySet[Replica]:
''' Operation replicas. '''
return Replica.objects.filter(original=self)
def getQ_references(self) -> QuerySet[Reference]:
''' Operation references. '''
return Reference.objects.filter(target=self)
def getQ_replica_original(self) -> list['Operation']:
''' Operation source for current replica. '''
return [x.original for x in Replica.objects.filter(replica=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. '''
@ -107,12 +107,12 @@ class Operation(Model):
return
self.result = result
self.save(update_fields=['result'])
for rep in self.getQ_replicas():
rep.replica.result = result
rep.replica.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 rep in self.getQ_replicas():
rep.replica.delete()
for ref in self.getQ_references():
ref.reference.delete()
super().delete(*args, **kwargs)

View File

@ -11,7 +11,7 @@ from .Block import Block
from .Inheritance import Inheritance
from .Layout import Layout
from .Operation import Operation, OperationType
from .Replica import Replica
from .Reference import Reference
from .Substitution import Substitution
@ -42,22 +42,6 @@ class OperationSchema:
''' OSS layout. '''
return Layout.objects.get(oss_id=itemID)
@staticmethod
def create_input(oss: LibraryItem, operation: Operation) -> RSFormCached:
''' Create input RSForm for given Operation. '''
schema = RSFormCached.create(
owner=oss.owner,
alias=operation.alias,
title=operation.title,
description=operation.description,
visible=False,
access_policy=oss.access_policy,
location=oss.location
)
Editor.set(schema.model.pk, oss.getQ_editors().values_list('pk', flat=True))
operation.setQ_result(schema.model)
return schema
def refresh_from_db(self) -> None:
''' Model wrapper. '''
self.model.refresh_from_db()
@ -67,15 +51,15 @@ class OperationSchema:
result = Operation.objects.create(oss=self.model, **kwargs)
return result
def create_replica(self, target: Operation) -> Operation:
''' Create Replica Operation. '''
def create_reference(self, target: Operation) -> Operation:
''' Create Reference Operation. '''
result = Operation.objects.create(
oss=self.model,
operation_type=OperationType.REPLICA,
operation_type=OperationType.REFERENCE,
result=target.result,
parent=target.parent
)
Replica.objects.create(replica=result, original=target)
Reference.objects.create(reference=result, target=target)
return result
def create_block(self, **kwargs) -> Block:
@ -96,6 +80,21 @@ class OperationSchema:
operation.save(update_fields=['parent'])
target.delete()
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 set_arguments(self, target: int, arguments: list[Operation]) -> None:
''' Set arguments of target Operation. '''
Argument.objects.filter(operation_id=target).delete()
@ -129,10 +128,10 @@ class OperationSchema:
.order_by('order')
if arg.argument.result_id is not None
]
if not schemas:
if len(schemas) == 0:
return
substitutions = operation.getQ_substitutions()
receiver = OperationSchema.create_input(self.model, operation)
receiver = self.create_input(operation)
parents: dict = {}
children: dict = {}

View File

@ -3,17 +3,31 @@
from typing import Optional
from apps.library.models import LibraryItem
from apps.rsform.models import Attribution, Constituenta, CstType, OrderManager, RSFormCached
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
from .OperationSchema import OperationSchema
from .OssCache import OssCache
from .PropagationEngine import PropagationEngine
from .Operation import Operation, OperationType
from .Reference import Reference
from .Substitution import Substitution
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
CstMapping = dict[str, Optional[Constituenta]]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
class OperationSchemaCached:
@ -21,20 +35,19 @@ class OperationSchemaCached:
def __init__(self, model: LibraryItem):
self.model = model
self.cache = OssCache(model.pk)
self.engine = PropagationEngine(self.cache)
self.cache = OssCache(self)
def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
''' Delete Replica Operation. '''
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]
original = self.cache.replica_original.get(target)
if original:
reference_target = self.cache.reference_target.get(target)
if reference_target:
for arg in operation.getQ_as_argument():
arg.argument_id = original
arg.argument_id = reference_target
arg.save()
self.cache.remove_operation(target)
operation.delete()
@ -44,11 +57,11 @@ class OperationSchemaCached:
''' Delete Operation. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
children = self.cache.extend_graph.outputs[target]
if operation.result is not None and children:
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.engine.on_delete_inherited(operation.pk, ids)
self._cascade_delete_inherited(operation.pk, ids)
else:
inheritance_to_delete: list[Inheritance] = []
for child_id in children:
@ -56,7 +69,7 @@ class OperationSchemaCached:
child_schema = self.cache.get_schema(child_operation)
if child_schema is None:
continue
self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
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)
@ -69,7 +82,7 @@ class OperationSchemaCached:
def set_input(self, target: int, schema: Optional[LibraryItem]) -> None:
''' Set input schema for operation. '''
operation = self.cache.operation_by_id[target]
has_children = bool(self.cache.extend_graph.outputs[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):
@ -105,7 +118,7 @@ class OperationSchemaCached:
processed.append(current.argument)
current.order = arguments.index(current.argument)
updated.append(current)
if deleted:
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)
@ -119,7 +132,7 @@ class OperationSchemaCached:
new_arg = Argument.objects.create(operation=operation, argument=arg, order=order)
self.cache.insert_argument(new_arg)
added.append(arg)
if added:
if len(added) > 0:
self.after_create_arguments(operation, added)
def set_substitutions(self, target: int, substitutes: list[dict]) -> None:
@ -134,14 +147,14 @@ class OperationSchemaCached:
x for x in substitutes
if x['original'] == current.original and x['substitution'] == current.substitution
]
if not subs:
if len(subs) == 0:
deleted.append(current)
else:
processed.append(subs[0])
if deleted:
if len(deleted) > 0:
if schema is not None:
for sub in deleted:
self.engine.undo_substitution(schema, sub)
self._undo_substitution(schema, sub)
else:
for sub in deleted:
self.cache.remove_substitution(sub)
@ -156,7 +169,22 @@ class OperationSchemaCached:
substitution=sub_item['substitution']
)
added.append(new_sub)
self._on_add_substitutions(schema, added)
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. '''
@ -169,11 +197,10 @@ class OperationSchemaCached:
.order_by('order')
if arg.argument.result_id is not None
]
if not schemas:
if len(schemas) == 0:
return False
substitutions = operation.getQ_substitutions()
receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk])
self.cache.insert_schema(receiver)
receiver = self._create_input(self.cache.operation_by_id[operation.pk])
parents: dict = {}
children: dict = {}
@ -204,7 +231,7 @@ class OperationSchemaCached:
receiver.reset_aliases()
receiver.resolve_all_text()
if self.cache.extend_graph.outputs[operation.pk]:
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'])
@ -216,7 +243,7 @@ class OperationSchemaCached:
self.cache.insert_schema(source)
self.cache.insert_schema(destination)
operation = self.cache.get_operation(destination.model.pk)
self.engine.undo_substitutions_cst(items, operation, destination)
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)
@ -255,27 +282,37 @@ class OperationSchemaCached:
exclude: Optional[list[int]] = None
) -> None:
''' Trigger cascade resolutions when new Constituenta is created. '''
source.cache.ensure_loaded()
self.cache.insert_schema(source)
alias_mapping = create_dependant_mapping(source, cst_list)
operation = self.cache.get_operation(source.model.pk)
self.engine.on_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.engine.on_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 = extract_data_references(data, old_data)
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
self.engine.on_update_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,
@ -283,15 +320,15 @@ class OperationSchemaCached:
mapping=alias_mapping
)
def before_delete_cst(self, operationID: int, target: list[int]) -> None:
def before_delete_cst(self, sourceID: int, target: list[int]) -> None:
''' Trigger cascade resolutions before Constituents are deleted. '''
operation = self.cache.get_operation(operationID)
self.engine.on_delete_inherited(operation.pk, target)
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.engine.on_before_substitute(operation.pk, substitutions)
self._cascade_before_substitute(substitutions, operation)
def before_delete_arguments(self, target: Operation, arguments: list[Operation]) -> None:
''' Trigger cascade resolutions before arguments are deleted. '''
@ -300,7 +337,7 @@ class OperationSchemaCached:
for argument in arguments:
parent_schema = self.cache.get_schema(argument)
if parent_schema is not None:
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
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. '''
@ -311,27 +348,336 @@ class OperationSchemaCached:
parent_schema = self.cache.get_schema(argument)
if parent_schema is None:
continue
self.engine.inherit_cst(
self._execute_inherit_cst(
target_operation=target.pk,
source=parent_schema,
items=list(parent_schema.constituentsQ().order_by('order')),
mapping={}
)
def after_create_attribution(self, schemaID: int, associations: list[Attribution],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when Attribution is created. '''
operation = self.cache.get_operation(schemaID)
self.engine.on_inherit_attribution(operation.pk, associations, exclude)
# 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 before_delete_attribution(self, schemaID: int, associations: list[Attribution]) -> None:
''' Trigger cascade resolutions when Attribution is deleted. '''
operation = self.cache.get_operation(schemaID)
self.engine.on_delete_attribution(operation.pk, associations)
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
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
''' Trigger cascade resolutions when Constituenta substitution is added. '''
if not added:
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:
@ -351,3 +697,185 @@ class OperationSchemaCached:
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

@ -1,188 +0,0 @@
''' Models: OSS API. '''
from typing import Optional
from apps.rsform.graph import Graph
from apps.rsform.models import RSFormCached
from .Argument import Argument
from .Inheritance import Inheritance
from .Operation import Operation, OperationType
from .Replica import Replica
from .Substitution import Substitution
class OssCache:
''' Cache for OSS data. '''
def __init__(self, item_id: int):
self._item_id = item_id
self._schemas: list[RSFormCached] = []
self._schema_by_id: dict[int, RSFormCached] = {}
self.operations = list(Operation.objects.filter(oss_id=item_id).only('result_id', 'operation_type'))
self.operation_by_id = {operation.pk: operation for operation in self.operations}
self.graph = Graph[int]()
self.extend_graph = Graph[int]()
for operation in self.operations:
self.graph.add_node(operation.pk)
self.extend_graph.add_node(operation.pk)
replicas = Replica.objects.filter(replica__oss_id=self._item_id).only('replica_id', 'original_id')
self.replica_original = {rep.replica_id: rep.original_id for rep in replicas}
arguments = Argument.objects \
.filter(operation__oss_id=self._item_id) \
.only('operation_id', 'argument_id') \
.order_by('order')
for argument in arguments:
self.graph.add_edge(argument.argument_id, argument.operation_id)
self.extend_graph.add_edge(argument.argument_id, argument.operation_id)
original = self.replica_original.get(argument.argument_id)
if original is not None:
self.extend_graph.add_edge(original, 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_id=self._item_id).only(
'operation_id', 'original_id', 'substitution_id', 'original__schema_id'):
self.substitutions[sub.operation_id].append(sub)
for item in Inheritance.objects.filter(operation__oss_id=self._item_id).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_schema_by_id(self, target: int) -> RSFormCached:
''' Get schema by Operation. '''
if target in self._schema_by_id:
return self._schema_by_id[target]
else:
schema = RSFormCached.from_id(target)
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.REPLICA:
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)
self.extend_graph.add_edge(argument.argument_id, argument.operation_id)
target = self.replica_original.get(argument.argument_id)
if target is not None:
self.extend_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)
self.extend_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.replica_original:
del self.replica_original[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)
self.extend_graph.remove_edge(argument.argument_id, argument.operation_id)
target = self.replica_original.get(argument.argument_id)
if target is not None:
if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
self.extend_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 _insert_new(self, schema: RSFormCached) -> None:
self._schemas.append(schema)
self._schema_by_id[schema.model.pk] = schema

View File

@ -1,386 +0,0 @@
''' Models: Change propagation engine. '''
from typing import Optional
from rest_framework.serializers import ValidationError
from apps.rsform.models import INSERT_LAST, Attribution, Constituenta, CstType, RSFormCached
from .Inheritance import Inheritance
from .Operation import Operation
from .OssCache import OssCache
from .Substitution import Substitution
from .utils import (
CstMapping,
CstSubstitution,
create_dependant_mapping,
cst_mapping_to_alias,
map_cst_update_data
)
class PropagationEngine:
''' OSS changes propagation engine. '''
def __init__(self, cache: OssCache):
self.cache = cache
def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
''' Trigger cascade resolutions when Constituenta type is changed. '''
children = self.cache.extend_graph.outputs[operation_id]
if not children:
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.on_change_cst_type(child_id, successor_id, ctype)
# pylint: disable=too-many-arguments, too-many-positional-arguments
def on_inherit_cst(
self,
target_operation: int,
source: RSFormCached,
items: list[Constituenta],
mapping: CstMapping,
exclude: Optional[list[int]] = None
) -> None:
''' Trigger cascade resolutions when Constituenta is inherited. '''
children = self.cache.extend_graph.outputs[target_operation]
if not children:
return
for child_id in children:
if not exclude or child_id not in exclude:
self.inherit_cst(child_id, source, items, mapping)
def inherit_cst(
self,
target_operation: int,
source: RSFormCached,
items: list[Constituenta],
mapping: CstMapping
) -> None:
''' Execute inheritance of Constituenta. '''
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 = cst_mapping_to_alias(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.on_inherit_cst(operation.pk, destination, new_cst_list, new_mapping)
# pylint: disable=too-many-arguments, too-many-positional-arguments
def on_update_cst(
self,
operation: int,
cst_id: int,
data: dict, old_data: dict,
mapping: CstMapping
) -> None:
''' Trigger cascade resolutions when Constituenta data is changed. '''
children = self.cache.extend_graph.outputs[operation]
if not children:
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 = cst_mapping_to_alias(new_mapping)
successor = child_schema.cache.by_id.get(successor_id)
if successor is None:
continue
new_data = map_cst_update_data(successor, data, old_data, alias_mapping)
if not new_data:
continue
new_old_data = child_schema.update_cst(successor.pk, new_data)
if not new_old_data:
continue
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
self.on_update_cst(
operation=child_id,
cst_id=successor_id,
data=new_data,
old_data=new_old_data,
mapping=new_mapping
)
def on_inherit_attribution(self, operationID: int,
items: list[Attribution],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when Attribution is inherited. '''
children = self.cache.extend_graph.outputs[operationID]
if not children:
return
for child_id in children:
if not exclude or child_id not in exclude:
self.inherit_association(child_id, items)
def inherit_association(self, target: int, items: list[Attribution]) -> None:
''' Execute inheritance of Associations. '''
operation = self.cache.operation_by_id[target]
if operation.result is None or not items:
return
self.cache.ensure_loaded_subs()
existing_associations = set(
Attribution.objects.filter(
container__schema_id=operation.result_id,
).values_list('container_id', 'attribute_id')
)
new_associations: list[Attribution] = []
for assoc in items:
new_container = self.cache.get_inheritor(assoc.container_id, target)
new_attribute = self.cache.get_inheritor(assoc.attribute_id, target)
if new_container is None or new_attribute is None \
or new_attribute == new_container \
or (new_container, new_attribute) in existing_associations:
continue
new_associations.append(Attribution(
container_id=new_container,
attribute_id=new_attribute
))
if new_associations:
new_associations = Attribution.objects.bulk_create(new_associations)
self.on_inherit_attribution(target, new_associations)
def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None:
''' Trigger cascade resolutions when Constituenta substitution is executed. '''
children = self.cache.extend_graph.outputs[operationID]
if not children:
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 not new_substitutions:
continue
self.on_before_substitute(child_operation.pk, new_substitutions)
child_schema.substitute(new_substitutions)
def on_delete_attribution(self, operationID: int, associations: list[Attribution]) -> None:
''' Trigger cascade resolutions when Attribution is deleted. '''
children = self.cache.extend_graph.outputs[operationID]
if not children:
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
deleted: list[Attribution] = []
for attr in associations:
new_container = self.cache.get_inheritor(attr.container_id, child_id)
new_attribute = self.cache.get_inheritor(attr.attribute_id, child_id)
if new_container is None or new_attribute is None:
continue
deleted_assoc = Attribution.objects.filter(
container=new_container,
attribute=new_attribute
)
if deleted_assoc.exists():
deleted.append(deleted_assoc[0])
if deleted:
self.on_delete_attribution(child_id, deleted)
Attribution.objects.filter(pk__in=[assoc.pk for assoc in deleted]).delete()
def on_delete_inherited(self, operation: int, target: list[int]) -> None:
''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
children = self.cache.extend_graph.outputs[operation]
if not children:
return
self.cache.ensure_loaded_subs()
for child_id in children:
self.delete_inherited(child_id, target)
def delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None:
''' Execute deletion of Constituenta inheritance. '''
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.on_delete_inherited(operation_id, target_ids)
if target_ids:
self.cache.remove_cst(operation_id, target_ids)
schema.delete_cst(target_ids)
def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
''' Undo substitutions for Constituents. '''
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:
''' Undo target substitution. '''
if ignore_parents is None:
ignore_parents = []
operation_id = target.operation_id
original_schema = self.cache.get_schema_by_id(target.original.schema_id)
dependant = []
for cst_id in original_schema.get_dependant([target.original_id]):
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 target.original_id not in ignore_parents:
full_cst = Constituenta.objects.get(pk=target.original_id)
cst_mapping = create_dependant_mapping(original_schema, [full_cst])
self.inherit_cst(operation_id, original_schema, [full_cst], cst_mapping)
new_original_id = self.cache.get_inheritor(target.original_id, operation_id)
assert new_original_id is not None
new_original = schema.cache.by_id[new_original_id]
if dependant:
substitution_id = self.cache.get_inheritor(target.substitution_id, operation_id)
assert substitution_id is not None
substitution_inheritor = schema.cache.by_id[substitution_id]
mapping = {substitution_inheritor.alias: new_original}
self._on_partial_mapping(mapping, dependant, operation_id, schema)
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 _transform_mapping(self, mapping: CstMapping, operation: Operation, schema: RSFormCached) -> CstMapping:
if not mapping:
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 _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 _on_partial_mapping(
self,
mapping: CstMapping,
target: list[int],
operation: int,
schema: RSFormCached
) -> None:
''' Trigger cascade resolutions when Constituents are partially mapped. '''
alias_mapping = cst_mapping_to_alias(mapping)
schema.apply_partial_mapping(alias_mapping, target)
children = self.cache.extend_graph.outputs[operation]
if not children:
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 not new_target:
continue
self._on_partial_mapping(new_mapping, new_target, child_id, child_schema)

View File

@ -2,7 +2,7 @@
from typing import Optional
from apps.library.models import LibraryItem, LibraryItemType
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
from apps.rsform.models import Constituenta, CstType, RSFormCached
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
@ -60,7 +60,7 @@ class PropagationFacade:
def before_substitute(sourceID: int, substitutions: CstSubstitution,
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before constituents are substituted. '''
if not substitutions:
if len(substitutions) == 0:
return
hosts = _get_oss_hosts(sourceID)
for host in hosts:
@ -73,29 +73,8 @@ class PropagationFacade:
if item.item_type != LibraryItemType.RSFORM:
return
hosts = _get_oss_hosts(item.pk)
if not hosts:
if len(hosts) == 0:
return
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
for host in hosts:
if exclude is None or host.pk not in exclude:
OperationSchemaCached(host).before_delete_cst(item.pk, ids)
@staticmethod
def after_create_attribution(sourceID: int, associations: list[Attribution],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions when Attribution is created. '''
hosts = _get_oss_hosts(sourceID)
for host in hosts:
if exclude is None or host.pk not in exclude:
OperationSchemaCached(host).after_create_attribution(sourceID, associations)
@staticmethod
def before_delete_attribution(sourceID: int,
associations: list[Attribution],
exclude: Optional[list[int]] = None) -> None:
''' Trigger cascade resolutions before Attribution is deleted. '''
hosts = _get_oss_hosts(sourceID)
for host in hosts:
if exclude is None or host.pk not in exclude:
OperationSchemaCached(host).before_delete_attribution(sourceID, associations)
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

@ -1,27 +0,0 @@
''' Models: Operation Replica in OSS. '''
from django.db.models import CASCADE, ForeignKey, Model
class Replica(Model):
''' Operation Replica. '''
replica = ForeignKey(
verbose_name='Реплика',
to='oss.Operation',
on_delete=CASCADE,
related_name='replicas'
)
original = ForeignKey(
verbose_name='Целевая Операция',
to='oss.Operation',
on_delete=CASCADE,
related_name='targets'
)
class Meta:
''' Model metadata. '''
verbose_name = 'Реплика'
verbose_name_plural = 'Реплики'
unique_together = [['replica', 'original']]
def __str__(self) -> str:
return f'{self.replica} -> {self.original}'

View File

@ -8,5 +8,5 @@ from .Operation import Operation, OperationType
from .OperationSchema import OperationSchema
from .OperationSchemaCached import OperationSchemaCached
from .PropagationFacade import PropagationFacade
from .Replica import Replica
from .Reference import Reference
from .Substitution import Substitution

View File

@ -1,79 +0,0 @@
''' Utils for OSS models. '''
from typing import Optional
from cctext import extract_entities
from apps.rsform.models import (
DELETED_ALIAS,
Constituenta,
RSFormCached,
extract_globals,
replace_entities,
replace_globals
)
CstMapping = dict[str, Optional[Constituenta]]
CstSubstitution = list[tuple[Constituenta, Constituenta]]
def cst_mapping_to_alias(mapping: CstMapping) -> dict[str, str]:
''' Convert constituenta mapping to alias mapping. '''
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 map_cst_update_data(cst: Constituenta, data: dict, old_data: dict, mapping: dict[str, str]) -> dict:
''' Map data for constituenta update. '''
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 extract_data_references(data: dict, old_data: dict) -> set[str]:
''' Extract references from data. '''
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 create_dependant_mapping(source: RSFormCached, cst_list: list[Constituenta]) -> CstMapping:
''' Create mapping for dependant Constituents. '''
if len(cst_list) == len(source.cache.constituents):
return {c.alias: c for c in source.cache.constituents}
inserted_aliases = [cst.alias for cst in cst_list]
depend_aliases: set[str] = set()
for item in cst_list:
depend_aliases.update(item.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
return alias_mapping

View File

@ -6,18 +6,18 @@ from .data_access import (
BlockSerializer,
CloneSchemaSerializer,
CreateBlockSerializer,
CreateReplicaSerializer,
CreateReferenceSerializer,
CreateSchemaSerializer,
CreateSynthesisSerializer,
DeleteBlockSerializer,
DeleteOperationSerializer,
DeleteReplicaSerializer,
DeleteReferenceSerializer,
ImportSchemaSerializer,
MoveItemsSerializer,
OperationSchemaSerializer,
OperationSerializer,
ReferenceSerializer,
RelocateConstituentsSerializer,
ReplicaSerializer,
SetOperationInputSerializer,
TargetOperationSerializer,
UpdateBlockSerializer,

View File

@ -20,7 +20,7 @@ from ..models import (
Layout,
Operation,
OperationType,
Replica,
Reference,
Substitution
)
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
@ -47,19 +47,19 @@ class BlockSerializer(StrictModelSerializer):
class ArgumentSerializer(StrictModelSerializer):
''' Serializer: Operation arguments. '''
''' Serializer: Operation data. '''
class Meta:
''' serializer metadata. '''
model = Argument
fields = ('operation', 'argument')
class ReplicaSerializer(StrictModelSerializer):
''' Serializer: Replica relation. '''
class ReferenceSerializer(StrictModelSerializer):
''' Serializer: Reference data. '''
class Meta:
''' serializer metadata. '''
model = Replica
fields = ('replica', 'original')
model = Reference
fields = ('reference', 'target')
class CreateBlockSerializer(StrictSerializer):
@ -251,15 +251,15 @@ class CloneSchemaSerializer(StrictSerializer):
raise serializers.ValidationError({
'source_operation': msg.operationResultEmpty(source_operation.alias)
})
if source_operation.operation_type == OperationType.REPLICA:
if source_operation.operation_type == OperationType.REFERENCE:
raise serializers.ValidationError({
'source_operation': msg.replicaNotAllowed()
'source_operation': msg.referenceTypeNotAllowed()
})
return attrs
class CreateReplicaSerializer(StrictSerializer):
''' Serializer: Create Replica operation. '''
class CreateReferenceSerializer(StrictSerializer):
''' Serializer: Create reference operation. '''
layout = serializers.ListField(child=NodeSerializer())
target = PKField(many=False, queryset=Operation.objects.all())
position = PositionSerializer()
@ -269,11 +269,11 @@ class CreateReplicaSerializer(StrictSerializer):
target = cast(Operation, attrs['target'])
if target.oss_id != oss.pk:
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
'target_operation': msg.operationNotInOSS()
})
if target.operation_type == OperationType.REPLICA:
if target.operation_type == OperationType.REFERENCE:
raise serializers.ValidationError({
'target': msg.replicaNotAllowed()
'target_operation': msg.referenceTypeNotAllowed()
})
return attrs
@ -328,10 +328,6 @@ class CreateSynthesisSerializer(StrictSerializer):
})
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
if len(schemas) != len(set(schemas)):
raise serializers.ValidationError({
'arguments': msg.duplicateSchemasInArguments()
})
substitutions = attrs['substitutions']
to_delete = {x['original'].pk for x in substitutions}
deleted = set()
@ -379,7 +375,6 @@ class UpdateOperationSerializer(StrictSerializer):
required=False
)
# pylint: disable=too-many-branches
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
parent = attrs['item_data'].get('parent')
@ -410,10 +405,6 @@ class UpdateOperationSerializer(StrictSerializer):
if 'substitutions' not in attrs:
return attrs
schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None]
if len(schemas) != len(set(schemas)):
raise serializers.ValidationError({
'arguments': msg.duplicateSchemasInArguments()
})
substitutions = attrs['substitutions']
to_delete = {x['original'].pk for x in substitutions}
deleted = set()
@ -441,7 +432,7 @@ class UpdateOperationSerializer(StrictSerializer):
class DeleteOperationSerializer(StrictSerializer):
''' Serializer: Delete non-replica operation. '''
''' Serializer: Delete non-reference operation. '''
layout = serializers.ListField(
child=NodeSerializer()
)
@ -456,15 +447,15 @@ class DeleteOperationSerializer(StrictSerializer):
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
})
if operation.operation_type == OperationType.REPLICA:
if operation.operation_type == OperationType.REFERENCE:
raise serializers.ValidationError({
'target': msg.replicaNotAllowed()
'target': msg.referenceTypeNotAllowed()
})
return attrs
class DeleteReplicaSerializer(StrictSerializer):
''' Serializer: Delete Replica operation. '''
class DeleteReferenceSerializer(StrictSerializer):
''' Serializer: Delete reference operation. '''
layout = serializers.ListField(
child=NodeSerializer()
)
@ -479,9 +470,9 @@ class DeleteReplicaSerializer(StrictSerializer):
raise serializers.ValidationError({
'target': msg.operationNotInOSS()
})
if operation.operation_type != OperationType.REPLICA:
if operation.operation_type != OperationType.REFERENCE:
raise serializers.ValidationError({
'target': msg.replicaRequired()
'target': msg.referenceTypeRequired()
})
return attrs
@ -544,8 +535,8 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitutions = serializers.ListField(
child=SubstitutionExSerializer()
)
replicas = serializers.ListField(
child=ReplicaSerializer()
references = serializers.ListField(
child=ReferenceSerializer()
)
layout = serializers.ListField(
child=NodeSerializer()
@ -564,7 +555,7 @@ class OperationSchemaSerializer(StrictModelSerializer):
result['blocks'] = []
result['arguments'] = []
result['substitutions'] = []
result['replicas'] = []
result['references'] = []
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
operation_data = OperationSerializer(operation).data
operation_result = operation.result
@ -587,8 +578,8 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitution_term=F('substitution__term_resolved'),
).order_by('pk'):
result['substitutions'].append(substitution)
for replication in Replica.objects.filter(original__oss=instance).order_by('pk'):
result['replicas'].append(ReplicaSerializer(replication).data)
for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
result['references'].append(ReferenceSerializer(reference).data)
return result

View File

@ -3,5 +3,5 @@ from .t_Argument import *
from .t_Inheritance import *
from .t_Layout import *
from .t_Operation import *
from .t_Replica import *
from .t_Reference import *
from .t_Substitution import *

View File

@ -1,12 +1,12 @@
''' Testing models: Replica. '''
''' Testing models: Reference. '''
from django.test import TestCase
from apps.oss.models import Operation, OperationSchema, OperationType, Replica
from apps.oss.models import Operation, OperationSchema, OperationType, Reference
from apps.rsform.models import RSForm
class TestReplica(TestCase):
''' Testing Replica model. '''
class TestReference(TestCase):
''' Testing Reference model. '''
def setUp(self):
@ -19,26 +19,26 @@ class TestReplica(TestCase):
)
self.operation2 = Operation.objects.create(
oss=self.oss.model,
operation_type=OperationType.REPLICA,
operation_type=OperationType.REFERENCE,
)
self.replicas = Replica.objects.create(
replica=self.operation2,
original=self.operation1
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.replicas), testStr)
self.assertEqual(str(self.reference), testStr)
def test_cascade_delete_operation(self):
self.assertEqual(Replica.objects.count(), 1)
self.assertEqual(Reference.objects.count(), 1)
self.operation2.delete()
self.assertEqual(Replica.objects.count(), 0)
self.assertEqual(Reference.objects.count(), 0)
def test_cascade_delete_target(self):
self.assertEqual(Replica.objects.count(), 1)
self.assertEqual(Reference.objects.count(), 1)
self.operation1.delete()
self.assertEqual(Replica.objects.count(), 0)
self.assertEqual(Reference.objects.count(), 0)

View File

@ -73,7 +73,7 @@ class TestChangeAttributes(EndpointTester):
def test_set_owner(self):
data = {'user': self.user3.pk}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.owned.model.refresh_from_db()
self.ks1.model.refresh_from_db()
@ -89,7 +89,7 @@ class TestChangeAttributes(EndpointTester):
def test_set_location(self):
data = {'location': '/U/temp'}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.owned.model.refresh_from_db()
self.ks1.model.refresh_from_db()
@ -105,7 +105,7 @@ class TestChangeAttributes(EndpointTester):
def test_set_access_policy(self):
data = {'access_policy': AccessPolicy.PROTECTED}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.owned.model.refresh_from_db()
self.ks1.model.refresh_from_db()
@ -124,7 +124,7 @@ class TestChangeAttributes(EndpointTester):
Editor.set(self.ks3.model.pk, [self.user2.pk, self.user.pk])
data = {'users': [self.user3.pk]}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.owned.model.refresh_from_db()
self.ks1.model.refresh_from_db()
@ -140,7 +140,7 @@ class TestChangeAttributes(EndpointTester):
def test_sync_from_result(self):
data = {'alias': 'KS111', 'title': 'New Title', 'description': 'New description'}
self.executeOK(data, item=self.ks1.model.pk)
self.executeOK(data=data, item=self.ks1.model.pk)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
@ -161,7 +161,7 @@ class TestChangeAttributes(EndpointTester):
'layout': self.layout_data
}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
self.ks3.model.refresh_from_db()
self.assertEqual(self.ks3.model.alias, data['item_data']['alias'])
self.assertEqual(self.ks3.model.title, data['item_data']['title'])

View File

@ -102,7 +102,7 @@ class TestChangeConstituents(EndpointTester):
'cst_type': CstType.BASE,
'definition_formal': 'X4 = X5'
}
response = self.executeCreated(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'])
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
self.assertEqual(self.ks1.constituentsQ().count(), 3)
@ -125,7 +125,7 @@ class TestChangeConstituents(EndpointTester):
'crucial': True,
}
}
response = self.executeOK(data, schema=self.ks1.model.pk)
response = self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X1.refresh_from_db()
d2.refresh_from_db()
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
@ -145,7 +145,7 @@ class TestChangeConstituents(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self):
data = {'items': [self.ks2X1.pk]}
response = self.executeOK(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)
self.ks2D1.refresh_from_db()
self.assertEqual(self.ks2.constituentsQ().count(), 1)
@ -161,7 +161,7 @@ class TestChangeConstituents(EndpointTester):
'original': self.ks1X1.pk,
'substitution': self.ks1X2.pk
}]}
self.executeOK(data, schema=self.ks1.model.pk)
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks1X2.refresh_from_db()
d2.refresh_from_db()
self.assertEqual(self.ks1.constituentsQ().count(), 1)

View File

@ -133,7 +133,7 @@ class TestChangeOperations(EndpointTester):
'layout': self.layout_data,
'target': self.operation2.pk
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
@ -155,7 +155,7 @@ class TestChangeOperations(EndpointTester):
'target': self.operation2.pk,
'input': None
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
@ -188,7 +188,7 @@ class TestChangeOperations(EndpointTester):
'target': self.operation2.pk,
'input': ks6.model.pk
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
ks4Dks6 = Constituenta.objects.get(as_child__parent_id=ks6D1.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
@ -234,7 +234,7 @@ class TestChangeOperations(EndpointTester):
'delete_schema': True
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -256,7 +256,7 @@ class TestChangeOperations(EndpointTester):
'delete_schema': True
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -278,7 +278,7 @@ class TestChangeOperations(EndpointTester):
'delete_schema': False
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks1.model.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
@ -317,7 +317,7 @@ class TestChangeOperations(EndpointTester):
]
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -343,7 +343,7 @@ class TestChangeOperations(EndpointTester):
'arguments': [self.operation1.pk],
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -356,7 +356,7 @@ class TestChangeOperations(EndpointTester):
self.assertEqual(self.ks5D4.definition_formal, r'DEL DEL X3 DEL D1 D2 D3')
data['arguments'] = [self.operation1.pk, self.operation2.pk]
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -381,7 +381,7 @@ class TestChangeOperations(EndpointTester):
'target': self.operation4.pk,
'layout': self.layout_data
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.operation4.refresh_from_db()
self.ks5.model.refresh_from_db()
self.assertNotEqual(self.operation4.result, None)
@ -408,7 +408,7 @@ class TestChangeOperations(EndpointTester):
'items': [ks6A1.pk]
}
self.executeOK(data)
self.executeOK(data=data)
ks6.model.refresh_from_db()
self.ks1.model.refresh_from_db()
self.ks4.model.refresh_from_db()
@ -438,7 +438,7 @@ class TestChangeOperations(EndpointTester):
'items': [self.ks1X2.pk]
}
self.executeOK(data)
self.executeOK(data=data)
ks6.model.refresh_from_db()
self.ks1.model.refresh_from_db()
self.ks4.model.refresh_from_db()

View File

@ -1,6 +1,6 @@
''' Testing API: Propagate changes through references in OSS. '''
from apps.oss.models import Inheritance, OperationSchema, OperationType
from apps.oss.models import OperationSchema, OperationType
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -50,7 +50,7 @@ class ReferencePropagationTestCase(EndpointTester):
operation_type=OperationType.INPUT,
result=self.ks2.model
)
self.operation3 = self.owned.create_replica(self.operation1)
self.operation3 = self.owned.create_reference(self.operation1)
self.operation4 = self.owned.create_operation(
alias='4',
@ -79,8 +79,8 @@ class ReferencePropagationTestCase(EndpointTester):
)
self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
self.owned.set_substitutions(self.operation5.pk, [{
'original': self.ks1X2,
'substitution': self.ks4X1
'original': self.ks4X1,
'substitution': self.ks1X2
}])
self.owned.execute_operation(self.operation5)
self.operation5.refresh_from_db()
@ -97,8 +97,8 @@ class ReferencePropagationTestCase(EndpointTester):
)
self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
self.owned.set_substitutions(self.operation6.pk, [{
'original': self.ks2X2,
'substitution': self.ks1X2
'original': self.ks2X1,
'substitution': self.ks1X1
}])
self.owned.execute_operation(self.operation6)
self.operation6.refresh_from_db()
@ -139,68 +139,8 @@ class ReferencePropagationTestCase(EndpointTester):
'layout': self.layout_data,
'target': self.operation1.pk
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.assertEqual(self.ks6.constituentsQ().count(), 4)
self.assertEqual(self.ks5.constituentsQ().count(), 5)
# self.assertEqual(self.ks5.constituentsQ().count(), 5)
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
def test_create_constituenta(self):
data = {
'alias': 'X3',
'cst_type': CstType.BASE,
}
response = self.executeCreated(data, schema=self.ks1.model.pk)
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
inherited = Constituenta.objects.filter(as_child__parent_id=new_cst.pk)
self.assertEqual(self.ks1.constituentsQ().count(), 4)
self.assertEqual(self.ks4.constituentsQ().count(), 7)
self.assertEqual(self.ks5.constituentsQ().count(), 11)
self.assertEqual(self.ks6.constituentsQ().count(), 7)
self.assertEqual(inherited.count(), 3)
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_constituenta(self):
data = {'items': [self.ks1X1.pk]}
response = self.executeOK(data, schema=self.ks1.model.pk)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
self.ks6D2.refresh_from_db()
self.assertEqual(self.ks4D2.definition_formal, r'X1 X2 X3 S1 D1')
self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 DEL S1 D1 D2 D3')
self.assertEqual(self.ks6D2.definition_formal, r'X1 DEL X3 S1 D1')
self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituentsQ().count(), 8)
self.assertEqual(self.ks6.constituentsQ().count(), 5)
@decl_endpoint('/api/oss/{item}/delete-replica', method='patch')
def test_delete_replica_redirection(self):
data = {
'layout': self.layout_data,
'target': self.operation3.pk,
'keep_connections': True,
'keep_constituents': False
}
self.executeOK(data, item=self.owned_id)
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-replica', method='patch')
def test_delete_replica_constituents(self):
data = {
'layout': self.layout_data,
'target': self.operation3.pk,
'keep_connections': False,
'keep_constituents': True
}
ks5X4 = Constituenta.objects.get(schema=self.ks5.model, alias='X4')
self.assertEqual(Inheritance.objects.filter(child=ks5X4).count(), 1)
self.executeOK(data, item=self.owned_id)
self.assertEqual(self.ks4.constituentsQ().count(), 6)
self.assertEqual(self.ks5.constituentsQ().count(), 9)
self.assertEqual(self.ks6.constituentsQ().count(), 7)
self.assertEqual(Inheritance.objects.filter(child=ks5X4).count(), 0)
# TODO: add more tests

View File

@ -134,7 +134,7 @@ class TestChangeSubstitutions(EndpointTester):
'original': self.ks1X1.pk,
'substitution': self.ks1X2.pk
}]}
self.executeOK(data, schema=self.ks1.model.pk)
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
@ -159,7 +159,7 @@ class TestChangeSubstitutions(EndpointTester):
'substitution': self.ks2X1.pk
}]
}
self.executeOK(data, schema=self.ks2.model.pk)
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
@ -179,7 +179,7 @@ class TestChangeSubstitutions(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_original(self):
data = {'items': [self.ks1X1.pk, self.ks1D1.pk]}
self.executeOK(data, schema=self.ks1.model.pk)
self.executeOK(data=data, schema=self.ks1.model.pk)
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()
subs1_2 = self.operation4.getQ_substitutions()
@ -194,7 +194,7 @@ class TestChangeSubstitutions(EndpointTester):
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
def test_delete_substitution(self):
data = {'items': [self.ks2S1.pk, self.ks2X2.pk]}
self.executeOK(data, schema=self.ks2.model.pk)
self.executeOK(data=data, schema=self.ks2.model.pk)
self.ks4D1.refresh_from_db()
self.ks4D2.refresh_from_db()
self.ks5D4.refresh_from_db()

View File

@ -81,9 +81,9 @@ class TestOssBlocks(EndpointTester):
'children_operations': [],
'children_blocks': []
}
self.executeNotFound(data, item=self.invalid_id)
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['blocks']), 3)
new_block = response.data['new_block']
layout = response.data['oss']['layout']
@ -94,9 +94,9 @@ class TestOssBlocks(EndpointTester):
self.assertEqual(block_node['height'], data['position']['height'])
self.operation1.refresh_from_db()
self.executeForbidden(data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data, item=self.unowned_id)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-block', method='post')
@ -118,13 +118,13 @@ class TestOssBlocks(EndpointTester):
'children_operations': [],
'children_blocks': []
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['parent'] = self.block3.pk
self.executeBadData(data)
self.executeBadData(data=data)
data['item_data']['parent'] = self.block1.pk
response = self.executeCreated(data)
response = self.executeCreated(data=data)
new_block = response.data['new_block']
block_data = next((block for block in response.data['oss']['blocks'] if block['id'] == new_block), None)
self.assertEqual(block_data['parent'], self.block1.pk)
@ -148,20 +148,20 @@ class TestOssBlocks(EndpointTester):
'children_operations': [self.invalid_id],
'children_blocks': []
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['children_operations'] = [self.operation3.pk]
self.executeBadData(data)
self.executeBadData(data=data)
data['children_operations'] = [self.block1.pk]
self.executeBadData(data)
self.executeBadData(data=data)
data['children_operations'] = [self.operation1.pk]
data['children_blocks'] = [self.operation1.pk]
self.executeBadData(data)
self.executeBadData(data=data)
data['children_blocks'] = [self.block1.pk]
response = self.executeCreated(data)
response = self.executeCreated(data=data)
new_block = response.data['new_block']
self.operation1.refresh_from_db()
self.block1.refresh_from_db()
@ -188,13 +188,13 @@ class TestOssBlocks(EndpointTester):
'children_operations': [],
'children_blocks': [self.block1.pk]
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['item_data']['parent'] = self.block1.pk
self.executeBadData(data)
self.executeBadData(data=data)
data['children_blocks'] = [self.block2.pk]
self.executeCreated(data)
self.executeCreated(data=data)
@decl_endpoint('/api/oss/{item}/delete-block', method='patch')
@ -206,26 +206,26 @@ class TestOssBlocks(EndpointTester):
data = {
'layout': self.layout_data
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.block3.pk
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.block2.pk
self.logout()
self.executeForbidden(data)
self.executeForbidden(data=data)
self.login()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation2.refresh_from_db()
self.assertEqual(len(response.data['blocks']), 1)
self.assertEqual(self.operation2.parent.pk, self.block1.pk)
data['target'] = self.block1.pk
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
self.assertEqual(len(response.data['blocks']), 0)
@ -246,25 +246,25 @@ class TestOssBlocks(EndpointTester):
'parent': None
},
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.block3.pk
self.toggle_admin(True)
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.block2.pk
self.logout()
self.executeForbidden(data)
self.executeForbidden(data=data)
self.login()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.block2.refresh_from_db()
self.assertEqual(self.block2.title, data['item_data']['title'])
self.assertEqual(self.block2.description, data['item_data']['description'])
self.assertEqual(self.block2.parent, data['item_data']['parent'])
data['layout'] = self.layout_data
self.executeOK(data)
self.executeOK(data=data)
@decl_endpoint('/api/oss/{item}/update-block', method='patch')
@ -280,13 +280,13 @@ class TestOssBlocks(EndpointTester):
'parent': self.block2.pk
},
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
# Create a deeper hierarchy: block1 -> block2 -> block3
self.block3 = self.owned.create_block(title='3', parent=self.block2)
# Try to set block1's parent to block3 (should fail, indirect cycle)
data['item_data']['parent'] = self.block3.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
# Setting block2's parent to block1 (valid, as block1 is not a descendant)
data = {
@ -297,4 +297,4 @@ class TestOssBlocks(EndpointTester):
'parent': self.block1.pk
},
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)

View File

@ -1,6 +1,6 @@
''' Testing API: Operation Schema - operations manipulation. '''
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Replica
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Reference
from apps.rsform.models import Constituenta, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -95,9 +95,9 @@ class TestOssOperations(EndpointTester):
'height': 50
}
}
self.executeNotFound(data, item=self.invalid_id)
self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation_id = response.data['new_operation']
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
@ -122,9 +122,9 @@ class TestOssOperations(EndpointTester):
self.assertEqual(schema.location, self.owned.model.location)
self.assertIn(self.user2, schema.getQ_editors())
self.executeForbidden(data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True)
self.executeCreated(data, item=self.unowned_id)
self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/clone-schema', method='post')
@ -141,10 +141,10 @@ class TestOssOperations(EndpointTester):
'height': 60
}
}
self.executeNotFound(data, item=self.invalid_id)
self.executeForbidden(data, item=self.unowned_id)
self.executeNotFound(data=data, item=self.invalid_id)
self.executeForbidden(data=data, item=self.unowned_id)
response = self.executeCreated(data, item=self.owned_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']
@ -171,7 +171,7 @@ class TestOssOperations(EndpointTester):
unrelated_data = dict(data)
unrelated_data['source_operation'] = self.unowned_operation.pk
self.executeBadData(unrelated_data, item=self.owned_id)
self.executeBadData(data=unrelated_data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/create-schema', method='post')
@ -194,23 +194,23 @@ class TestOssOperations(EndpointTester):
}
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
block_unowned = self.unowned.create_block(title='TestBlock1')
data['item_data']['parent'] = block_unowned.id
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
block_owned = self.owned.create_block(title='TestBlock2')
data['item_data']['parent'] = block_owned.id
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
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(len(response.data['oss']['operations']), 4)
self.assertEqual(new_operation['parent'], block_owned.id)
@decl_endpoint('/api/oss/{item}/create-replica', method='post')
def test_create_replica(self):
@decl_endpoint('/api/oss/{item}/create-reference', method='post')
def test_create_reference(self):
self.populateData()
data = {
'target': self.invalid_id,
@ -222,20 +222,20 @@ class TestOssOperations(EndpointTester):
'height': 40
}
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.unowned_operation.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation1.pk
response = self.executeCreated(data, item=self.owned_id)
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.REPLICA)
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 = Replica.objects.filter(replica_id=new_operation_id, original_id=self.operation1.pk).first()
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())
@ -260,7 +260,7 @@ class TestOssOperations(EndpointTester):
'arguments': [self.operation1.pk, self.operation3.pk],
'substitutions': []
}
response = self.executeCreated(data, item=self.owned_id)
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)
@ -270,37 +270,6 @@ class TestOssOperations(EndpointTester):
self.assertNotEqual(new_operation['result'], None)
@decl_endpoint('/api/oss/{item}/create-synthesis', method='post')
def test_create_synthesis_replicas(self):
self.populateData()
operation4 = self.owned.create_replica(self.operation1)
operation5 = self.owned.create_replica(self.operation1)
data = {
'item_data': {
'alias': 'Test5',
'title': 'Test title',
'description': '',
'parent': None
},
'layout': self.layout_data,
'position': {
'x': 1,
'y': 1,
'width': 500,
'height': 50
},
'arguments': [self.operation1.pk, operation4.pk],
'substitutions': []
}
self.executeBadData(data, item=self.owned_id)
data['arguments'] = [operation4.pk, operation5.pk]
self.executeBadData(data, item=self.owned_id)
data['arguments'] = [operation4.pk, self.operation3.pk]
self.executeCreated(data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self):
self.populateData()
@ -310,19 +279,19 @@ class TestOssOperations(EndpointTester):
data = {
'layout': self.layout_data
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.unowned_operation.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data, item=self.unowned_id)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data)
response = self.executeOK(data=data)
layout = response.data['layout']
deleted_items = [item for item in layout if item['nodeID'] == 'o' + str(data['target'])]
self.assertEqual(len(response.data['operations']), 2)
@ -332,34 +301,34 @@ class TestOssOperations(EndpointTester):
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_reference_operation_invalid(self):
self.populateData()
reference_operation = self.owned.create_replica(self.operation1)
reference_operation = self.owned.create_reference(self.operation1)
data = {
'layout': self.layout_data,
'target': reference_operation.pk
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/delete-replica', method='patch')
def test_delete_replica_operation(self):
@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, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
reference_operation = self.owned.create_replica(self.operation1)
self.assertEqual(len(self.operation1.getQ_replicas()), 1)
reference_operation = self.owned.create_reference(self.operation1)
self.assertEqual(len(self.operation1.getQ_references()), 1)
data['target'] = reference_operation.pk
self.executeForbidden(data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.unowned_id)
data['target'] = self.operation1.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = reference_operation.pk
self.executeOK(data, item=self.owned_id)
self.assertEqual(len(self.operation1.getQ_replicas()), 0)
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')
@ -370,22 +339,22 @@ class TestOssOperations(EndpointTester):
data = {
'layout': self.layout_data
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data, item=self.unowned_id)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.description = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.save()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
@ -395,10 +364,10 @@ class TestOssOperations(EndpointTester):
self.assertEqual(new_schema['description'], self.operation1.description)
data['target'] = self.operation3.pk
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.unowned_operation.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/set-input', method='patch')
@ -409,17 +378,17 @@ class TestOssOperations(EndpointTester):
data = {
'layout': self.layout_data
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.operation1.pk
data['input'] = None
self.toggle_admin(True)
self.executeBadData(data, item=self.unowned_id)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, None)
@ -428,7 +397,7 @@ class TestOssOperations(EndpointTester):
self.ks1.model.title = 'Test421'
self.ks1.model.description = 'TestComment42'
self.ks1.model.save()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks1.model)
self.assertEqual(self.operation1.alias, self.ks1.model.alias)
@ -446,7 +415,7 @@ class TestOssOperations(EndpointTester):
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
self.ks2.model.visible = False
self.ks2.model.save(update_fields=['visible'])
@ -455,7 +424,7 @@ class TestOssOperations(EndpointTester):
'target': self.operation2.pk,
'input': None
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.operation2.refresh_from_db()
self.ks2.model.refresh_from_db()
self.assertEqual(self.operation2.result, None)
@ -466,7 +435,7 @@ class TestOssOperations(EndpointTester):
'target': self.operation1.pk,
'input': self.ks2.model.pk
}
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.result, self.ks2.model)
@ -494,16 +463,16 @@ class TestOssOperations(EndpointTester):
}
]
}
self.executeBadData(data)
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2X1.pk
self.toggle_admin(True)
self.executeBadData(data, item=self.unowned_id)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
@ -518,11 +487,11 @@ class TestOssOperations(EndpointTester):
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
data['layout'] = self.layout_data
self.executeOK(data)
self.executeOK(data=data)
data_bad = dict(data)
data_bad['target'] = self.unowned_operation.pk
self.executeBadData(data_bad, item=self.owned_id)
self.executeBadData(data=data_bad, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
@ -539,10 +508,10 @@ class TestOssOperations(EndpointTester):
},
'layout': self.layout_data
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation1.pk
response = self.executeOK(data)
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
@ -554,7 +523,7 @@ class TestOssOperations(EndpointTester):
# 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_bad, item=self.owned_id)
self.executeBadData(data=data_bad, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
@ -583,7 +552,7 @@ class TestOssOperations(EndpointTester):
}
]
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/oss/{item}/execute-operation', method='post')
@ -595,19 +564,19 @@ class TestOssOperations(EndpointTester):
'layout': self.layout_data,
'target': self.operation1.pk
}
self.executeBadData(data)
self.executeBadData(data=data)
data['target'] = self.unowned_operation.pk
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['target'] = self.operation3.pk
self.toggle_admin(True)
self.executeBadData(data, item=self.unowned_id)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeOK(data)
self.executeOK(data=data)
self.operation3.refresh_from_db()
schema = self.operation3.result
self.assertEqual(schema.alias, self.operation3.alias)
@ -644,7 +613,7 @@ class TestOssOperations(EndpointTester):
'source': target_ks.model.pk,
'clone_source': False
}
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
new_operation_id = response.data['new_operation']
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
layout = response.data['oss']['layout']
@ -684,7 +653,7 @@ class TestOssOperations(EndpointTester):
'source': self.ks2.model.pk,
'clone_source': True
}
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
new_operation_id = response.data['new_operation']
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
layout = response.data['oss']['layout']
@ -725,13 +694,13 @@ class TestOssOperations(EndpointTester):
# 'source' missing
'clone_source': False
}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
# Invalid source
data['source'] = self.invalid_id
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
# Invalid OSS
data['source'] = self.ks1.model.pk
self.executeNotFound(data, item=self.invalid_id)
self.executeNotFound(data=data, item=self.invalid_id)
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
def test_import_schema_permissions(self):
@ -752,8 +721,8 @@ class TestOssOperations(EndpointTester):
}
# Not an editor
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.executeForbidden(data=data, item=self.owned_id)
# As admin
self.login()
self.toggle_admin(True)
self.executeCreated(data, item=self.owned_id)
self.executeCreated(data=data, item=self.owned_id)

View File

@ -126,7 +126,7 @@ class TestOssViewset(EndpointTester):
self.executeBadData(item=self.owned_id)
data = {'data': []}
self.executeOK(data)
self.executeOK(data=data)
data = {'data': [
{'nodeID': 'o' + str(self.operation1.pk), 'x': 42.1, 'y': 1337, 'width': 150, 'height': 40},
@ -134,15 +134,15 @@ class TestOssViewset(EndpointTester):
{'nodeID': 'o' + str(self.operation3.pk), 'x': 36.1, 'y': 1435, 'width': 150, 'height': 40}
]}
self.toggle_admin(True)
self.executeOK(data, item=self.unowned_id)
self.executeOK(data=data, item=self.unowned_id)
self.toggle_admin(False)
self.executeOK(data, item=self.owned_id)
self.executeOK(data=data, item=self.owned_id)
self.owned.model.refresh_from_db()
self.assertEqual(OperationSchema.layoutQ(self.owned_id).data, data['data'])
self.executeForbidden(data, item=self.unowned_id)
self.executeForbidden(data, item=self.private_id)
self.executeForbidden(data=data, item=self.unowned_id)
self.executeForbidden(data=data, item=self.private_id)
@decl_endpoint('/api/oss/get-predecessor', method='post')
@ -155,13 +155,13 @@ class TestOssViewset(EndpointTester):
self.ks3 = RSForm(self.operation3.result)
self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
self.executeBadData({'target': self.invalid_id})
self.executeBadData(data={'target': self.invalid_id})
response = self.executeOK({'target': self.ks1X1.pk})
response = self.executeOK(data={'target': self.ks1X1.pk})
self.assertEqual(response.data['id'], self.ks1X1.pk)
self.assertEqual(response.data['schema'], self.ks1.model.pk)
response = self.executeOK({'target': self.ks3X2.pk})
response = self.executeOK(data={'target': self.ks3X2.pk})
self.assertEqual(response.data['id'], self.ks1X2.pk)
self.assertEqual(response.data['schema'], self.ks1.model.pk)
@ -180,10 +180,10 @@ class TestOssViewset(EndpointTester):
'operations': [self.operation1.pk, self.operation2.pk],
'destination': block2.pk
}
self.executeBadData(data)
self.executeBadData(data=data)
data['destination'] = block1.pk
self.executeOK(data)
self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
block2.refresh_from_db()
@ -193,7 +193,7 @@ class TestOssViewset(EndpointTester):
self.assertEqual(block2.parent.pk, block1.pk)
data['destination'] = None
self.executeOK(data)
self.executeOK(data=data)
self.operation1.refresh_from_db()
self.operation2.refresh_from_db()
block2.refresh_from_db()
@ -217,7 +217,7 @@ class TestOssViewset(EndpointTester):
'operations': [],
'destination': block3.pk
}
self.executeBadData(data)
self.executeBadData(data=data)
@decl_endpoint('/api/oss/relocate-constituents', method='post')
@ -236,35 +236,35 @@ class TestOssViewset(EndpointTester):
'destination': self.invalid_id,
'items': []
}
self.executeBadData(data)
self.executeBadData(data=data)
# empty items
data = {
'destination': self.ks1.model.pk,
'items': []
}
self.executeBadData(data)
self.executeBadData(data=data)
# source == destination
data = {
'destination': self.ks1.model.pk,
'items': [self.ks1X1.pk]
}
self.executeBadData(data)
self.executeBadData(data=data)
# moving inherited
data = {
'destination': self.ks1.model.pk,
'items': [self.ks3X2.pk]
}
self.executeBadData(data)
self.executeBadData(data=data)
# source and destination are not connected
data = {
'destination': self.ks2.model.pk,
'items': [self.ks1X1.pk]
}
self.executeBadData(data)
self.executeBadData(data=data)
data = {
'destination': self.ks3.model.pk,
@ -272,14 +272,14 @@ class TestOssViewset(EndpointTester):
}
self.ks3X2.refresh_from_db()
self.assertEqual(self.ks3X2.convention, 'test')
self.executeOK(data)
self.executeOK(data=data)
self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1X2.pk).exists())
data = {
'destination': self.ks1.model.pk,
'items': [self.ks3X10.pk]
}
self.executeOK(data)
self.executeOK(data=data)
self.assertTrue(Constituenta.objects.filter(as_parent__child_id=self.ks3X10.pk).exists())
self.ks1X3 = Constituenta.objects.get(as_parent__child_id=self.ks3X10.pk)
self.assertEqual(self.ks1X3.convention, 'test2')

View File

@ -65,11 +65,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'create_schema',
'clone_schema',
'import_schema',
'create_replica',
'create_reference',
'create_synthesis',
'update_operation',
'delete_operation',
'delete_replica',
'delete_reference',
'create_input',
'set_input',
'execute_operation',
@ -140,7 +140,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def create_block(self, request: Request, pk) -> HttpResponse:
''' Create Block. '''
item = self._get_item()
serializer = s.CreateBlockSerializer(data=request.data, context={'oss': item})
serializer = s.CreateBlockSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
position = serializer.validated_data['position']
@ -158,11 +161,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'height': position['height'],
})
m.Layout.update_data(pk, layout)
if children_blocks:
if len(children_blocks) > 0:
for block in children_blocks:
block.parent = new_block
m.Block.objects.bulk_update(children_blocks, ['parent'])
if children_operations:
if len(children_operations) > 0:
for operation in children_operations:
operation.parent = new_block
m.Operation.objects.bulk_update(children_operations, ['parent'])
@ -191,7 +194,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def update_block(self, request: Request, pk) -> HttpResponse:
''' Update Block. '''
item = self._get_item()
serializer = s.UpdateBlockSerializer(data=request.data, context={'oss': item})
serializer = s.UpdateBlockSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
block: m.Block = cast(m.Block, serializer.validated_data['target'])
@ -228,7 +234,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def delete_block(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Block. '''
item = self._get_item()
serializer = s.DeleteBlockSerializer(data=request.data, context={'oss': item})
serializer = s.DeleteBlockSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
block = cast(m.Block, serializer.validated_data['target'])
layout = serializer.validated_data['layout']
@ -260,7 +269,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def move_items(self, request: Request, pk) -> HttpResponse:
''' Move items to another parent. '''
item = self._get_item()
serializer = s.MoveItemsSerializer(data=request.data, context={'oss': item})
serializer = s.MoveItemsSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
@ -294,7 +306,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def create_schema(self, request: Request, pk) -> HttpResponse:
''' Create schema. '''
item = self._get_item()
serializer = s.CreateSchemaSerializer(data=request.data, context={'oss': item})
serializer = s.CreateSchemaSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
position = serializer.validated_data['position']
@ -312,7 +327,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'height': position['height']
})
m.Layout.update_data(pk, layout)
m.OperationSchema.create_input(item, new_operation)
oss.create_input(new_operation)
item.save(update_fields=['time_update'])
return Response(
@ -339,7 +354,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def clone_schema(self, request: Request, pk) -> HttpResponse:
''' Clone schema. '''
item = self._get_item()
serializer = s.CloneSchemaSerializer(data=request.data, context={'oss': 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']
@ -406,7 +424,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def import_schema(self, request: Request, pk) -> HttpResponse:
''' Create operation with existing schema. '''
item = self._get_item()
serializer = s.ImportSchemaSerializer(data=request.data, context={'oss': item})
serializer = s.ImportSchemaSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
position = serializer.validated_data['position']
@ -444,9 +465,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@extend_schema(
summary='create replica for operation',
summary='create reference for operation',
tags=['OSS'],
request=s.CreateReplicaSerializer(),
request=s.CreateReferenceSerializer(),
responses={
c.HTTP_201_CREATED: s.OperationCreatedResponse,
c.HTTP_400_BAD_REQUEST: None,
@ -454,11 +475,14 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'], url_path='create-replica')
def create_replica(self, request: Request, pk) -> HttpResponse:
''' Replicate schema. '''
@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.CreateReplicaSerializer(data=request.data, context={'oss': 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']
@ -466,7 +490,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic():
oss = m.OperationSchema(item)
target = cast(m.Operation, serializer.validated_data['target'])
new_operation = oss.create_replica(target)
new_operation = oss.create_reference(target)
layout.append({
'nodeID': 'o' + str(new_operation.pk),
'x': position['x'],
@ -500,7 +524,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def create_synthesis(self, request: Request, pk) -> HttpResponse:
''' Create Synthesis operation from arguments. '''
item = self._get_item()
serializer = s.CreateSynthesisSerializer(data=request.data, context={'oss': item})
serializer = s.CreateSynthesisSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
position = serializer.validated_data['position']
@ -546,7 +573,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def update_operation(self, request: Request, pk) -> HttpResponse:
''' Update Operation arguments and parameters. '''
item = self._get_item()
serializer = s.UpdateOperationSerializer(data=request.data, context={'oss': item})
serializer = s.UpdateOperationSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
@ -598,7 +628,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def delete_operation(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Operation. '''
item = self._get_item()
serializer = s.DeleteOperationSerializer(data=request.data, context={'oss': item})
serializer = s.DeleteOperationSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
operation = cast(m.Operation, serializer.validated_data['target'])
old_schema = operation.result
@ -624,9 +657,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
)
@extend_schema(
summary='delete replica',
summary='delete reference',
tags=['OSS'],
request=s.DeleteReplicaSerializer(),
request=s.DeleteReferenceSerializer(),
responses={
c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None,
@ -634,22 +667,23 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='delete-replica')
def delete_replica(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Replica Operation. '''
@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.DeleteReplicaSerializer(data=request.data, context={'oss': item})
serializer = s.DeleteReferenceSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
operation = cast(m.Operation, serializer.validated_data['target'])
keep_connections = serializer.validated_data['keep_connections']
keep_constituents = serializer.validated_data['keep_constituents']
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_replica(operation.pk, keep_connections, keep_constituents)
oss.delete_reference(operation.pk, serializer.validated_data['keep_connections'])
item.save(update_fields=['time_update'])
return Response(
@ -672,7 +706,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def create_input(self, request: Request, pk) -> HttpResponse:
''' Create input RSForm. '''
item = self._get_item()
serializer = s.TargetOperationSerializer(data=request.data, context={'oss': item})
serializer = s.TargetOperationSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if len(operation.getQ_arguments()) > 0:
@ -686,8 +723,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
layout = serializer.validated_data['layout']
with transaction.atomic():
oss = m.OperationSchema(item)
m.Layout.update_data(pk, layout)
schema = m.OperationSchema.create_input(item, operation)
schema = oss.create_input(operation)
item.save(update_fields=['time_update'])
return Response(
@ -713,7 +751,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def set_input(self, request: Request, pk) -> HttpResponse:
''' Set input schema for target operation. '''
item = self._get_item()
serializer = s.SetOperationInputSerializer(data=request.data, context={'oss': item})
serializer = s.SetOperationInputSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
layout = serializer.validated_data['layout']
@ -762,7 +803,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
def execute_operation(self, request: Request, pk) -> HttpResponse:
''' Execute operation. '''
item = self._get_item()
serializer = s.TargetOperationSerializer(data=request.data, context={'oss': item})
serializer = s.TargetOperationSerializer(
data=request.data,
context={'oss': item}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.SYNTHESIS:

View File

@ -24,7 +24,7 @@ class TestPromptTemplateViewSet(EndpointTester):
'text': 'prompt text',
'is_shared': False
}
response = self.executeCreated(data)
response = self.executeCreated(data=data)
self.assertEqual(response.data['label'], 'Test')
self.assertEqual(response.data['owner'], self.user.pk)
@ -38,7 +38,7 @@ class TestPromptTemplateViewSet(EndpointTester):
'text': 'prompt text',
'is_shared': True
}
response = self.executeCreated(data)
response = self.executeCreated(data=data)
self.assertTrue(response.data['is_shared'])
@ -50,21 +50,21 @@ class TestPromptTemplateViewSet(EndpointTester):
'text': 'prompt text',
'is_shared': True
}
response = self.executeBadData(data)
response = self.executeBadData(data=data)
self.assertIn('is_shared', response.data)
@decl_endpoint('/api/prompts/{item}/', method='patch')
def test_update_prompt_owner(self):
prompt = PromptTemplate.objects.create(owner=self.user, label='ToUpdate', description='', text='t')
response = self.executeOK({'label': 'Updated'}, item=prompt.id)
response = self.executeOK(data={'label': 'Updated'}, item=prompt.id)
self.assertEqual(response.data['label'], 'Updated')
@decl_endpoint('/api/prompts/{item}/', method='patch')
def test_update_prompt_not_owner_forbidden(self):
prompt = PromptTemplate.objects.create(owner=self.admin, label='Other', description='', text='t')
response = self.executeForbidden({'label': 'Updated'}, item=prompt.id)
response = self.executeForbidden(data={'label': 'Updated'}, item=prompt.id)
@decl_endpoint('/api/prompts/{item}/', method='delete')
@ -112,4 +112,4 @@ class TestPromptTemplateViewSet(EndpointTester):
is_shared=True
)
self.client.force_authenticate(user=self.user)
response = self.executeForbidden({'label': 'Nope'}, item=prompt.id)
response = self.executeForbidden(data={'label': 'Nope'}, item=prompt.id)

View File

@ -10,11 +10,3 @@ class ConstituentaAdmin(admin.ModelAdmin):
ordering = ['schema', 'order']
list_display = ['schema', 'order', 'alias', 'term_resolved', 'definition_resolved', 'crucial']
search_fields = ['term_resolved', 'definition_resolved']
@admin.register(models.Attribution)
class AttributionAdmin(admin.ModelAdmin):
''' Admin model: Attribution. '''
ordering = ['container__schema', 'container', 'attribute']
list_display = ['container__schema__alias', 'container__alias', 'attribute__alias']
search_fields = ['container', 'attribute']

View File

@ -110,7 +110,7 @@ class Graph(Generic[ItemType]):
order = self.topological_order()
order.reverse()
for node_id in order:
if not self.inputs[node_id]:
if len(self.inputs[node_id]) == 0:
continue
for parent in self.inputs[node_id]:
result[parent] = result[parent] + [id for id in result[node_id] if id not in result[parent]]
@ -124,7 +124,7 @@ class Graph(Generic[ItemType]):
if node_id in marked:
continue
to_visit: list[ItemType] = [node_id]
while to_visit:
while len(to_visit) > 0:
node = to_visit[-1]
if node in marked:
if node not in result:
@ -132,7 +132,7 @@ class Graph(Generic[ItemType]):
to_visit.remove(node)
else:
marked.add(node)
if not self.outputs[node]:
if len(self.outputs[node]) <= 0:
continue
for child_id in self.outputs[node]:
if child_id not in marked:

View File

@ -1,32 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-09 10:55
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0004_constituenta_crucial'),
]
operations = [
migrations.AlterField(
model_name='constituenta',
name='cst_type',
field=models.CharField(choices=[('nominal', 'Nominal'), ('basic', 'Base'), ('constant', 'Constant'), ('structure', 'Structured'), ('axiom', 'Axiom'), ('term', 'Term'), ('function', 'Function'), ('predicate', 'Predicate'), ('theorem', 'Theorem')], default='basic', max_length=10, verbose_name='Тип'),
),
migrations.CreateModel(
name='Association',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('associate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_associate', to='rsform.constituenta', verbose_name='Ассоциированная конституента')),
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_container', to='rsform.constituenta', verbose_name='Составная конституента')),
],
options={
'verbose_name': 'Ассоциация конституент',
'verbose_name_plural': 'Ассоциации конституент',
'unique_together': {('container', 'associate')},
},
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-21 11:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0005_alter_constituenta_cst_type_association'),
]
operations = [
migrations.CreateModel(
name='Attribution',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_attribute', to='rsform.constituenta', verbose_name='Атрибутирующая конституента')),
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='as_container', to='rsform.constituenta', verbose_name='Составная конституента')),
],
options={
'verbose_name': 'Атрибутирование конституент',
'verbose_name_plural': 'Атрибутирования конституент',
'unique_together': {('container', 'attribute')},
},
),
migrations.DeleteModel(
name='Association',
),
]

View File

@ -1,28 +0,0 @@
''' Models: Attribution of nominal constituents. '''
from django.db.models import CASCADE, ForeignKey, Model
class Attribution(Model):
''' Attribution links nominal constituent to its content.'''
container = ForeignKey(
verbose_name='Составная конституента',
to='rsform.Constituenta',
on_delete=CASCADE,
related_name='as_container'
)
attribute = ForeignKey(
verbose_name='Атрибутирующая конституента',
to='rsform.Constituenta',
on_delete=CASCADE,
related_name='as_attribute'
)
class Meta:
''' Model metadata. '''
verbose_name = 'Атрибутирование конституент'
verbose_name_plural = 'Атрибутирования конституент'
unique_together = [['container', 'attribute']]
def __str__(self) -> str:
return f'{self.container} -> {self.attribute}'

View File

@ -16,9 +16,9 @@ from django.db.models import (
from ..utils import apply_pattern
_RE_GLOBALS = r'[XCSADFPTN]\d+' # cspell:disable-line
_RE_GLOBALS = r'[XCSADFPT]\d+' # cspell:disable-line
_REF_ENTITY_PATTERN = re.compile(r'@{([^0-9\-].*?)\|.*?}')
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPTN][0-9]+)') # cspell:disable-line
_GLOBAL_ID_PATTERN = re.compile(r'([XCSADFPT][0-9]+)') # cspell:disable-line
def extract_globals(expression: str) -> set[str]:
@ -38,7 +38,6 @@ def replace_entities(expression: str, mapping: dict[str, str]) -> str:
class CstType(TextChoices):
''' Type of constituenta. '''
NOMINAL = 'nominal'
BASE = 'basic'
CONSTANT = 'constant'
STRUCTURED = 'structure'

View File

@ -48,7 +48,7 @@ class OrderManager:
continue
result.append(cst)
children = self._semantic[cst.pk]['children']
if not children:
if len(children) == 0:
continue
for child in self._items:
if child.pk in children:

View File

@ -158,12 +158,12 @@ class RSForm:
graph_terms = RSForm.graph_term(cst_list, cst_by_alias)
expansion = graph_terms.expand_outputs(changed)
expanded_change = changed + expansion
update_list: list[Constituenta] = []
if resolver is None:
resolver = RSForm.resolver_from_list(cst_list)
if expansion:
resolved_terms: list[Constituenta] = []
if len(expansion) > 0:
for cst_id in graph_terms.topological_order():
if cst_id not in expansion:
continue
@ -172,20 +172,21 @@ class RSForm:
if resolved == resolver.context[cst.alias].get_nominal():
continue
cst.set_term_resolved(resolved)
resolved_terms.append(cst)
update_list.append(cst)
resolver.context[cst.alias] = Entity(cst.alias, resolved)
Constituenta.objects.bulk_update(resolved_terms, ['term_resolved'])
Constituenta.objects.bulk_update(update_list, ['term_resolved'])
graph_defs = RSForm.graph_text(cst_list, cst_by_alias)
update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
if update_defs:
resolved_defs: list[Constituenta] = []
for cst_id in update_defs:
cst = cst_by_id[cst_id]
resolved = resolver.resolve(cst.definition_raw)
cst.definition_resolved = resolved
resolved_defs.append(cst)
Constituenta.objects.bulk_update(resolved_defs, ['definition_resolved'])
update_list = []
if len(update_defs) == 0:
return
for cst_id in update_defs:
cst = cst_by_id[cst_id]
resolved = resolver.resolve(cst.definition_raw)
cst.definition_resolved = resolved
update_list.append(cst)
Constituenta.objects.bulk_update(update_list, ['definition_resolved'])
def constituentsQ(self) -> QuerySet[Constituenta]:
''' Get QuerySet containing all constituents of current RSForm. '''
@ -262,7 +263,7 @@ class RSForm:
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
''' Execute constituenta substitution. '''
if not substitutions:
if len(substitutions) < 1:
return
mapping = {}
deleted: list[int] = []

View File

@ -223,7 +223,7 @@ class RSFormCached:
def substitute(self, substitutions: list[tuple[Constituenta, Constituenta]]) -> None:
''' Execute constituenta substitution. '''
if not substitutions:
if len(substitutions) < 1:
return
self.cache.ensure_loaded_terms()
mapping = {}

View File

@ -126,7 +126,7 @@ class SemanticInfo:
return sources
def _need_check_head(self, sources: set[int], head: str) -> bool:
if not sources:
if len(sources) == 0:
return True
elif len(sources) != 1:
return False

View File

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

View File

@ -37,7 +37,6 @@ def get_type_prefix(cst_type: str) -> str:
case CstType.FUNCTION: return 'F'
case CstType.PREDICATE: return 'P'
case CstType.THEOREM: return 'T'
case CstType.NOMINAL: return 'N'
return 'X'
@ -79,7 +78,7 @@ def guess_type(alias: str) -> CstType:
def _get_structure_prefix(alias: str, expression: str, parse: dict) -> Tuple[str, str]:
''' Generate prefix and alias for structure generation. '''
args = parse['args']
if not args:
if len(args) == 0:
return (alias, '')
prefix = expression[0:expression.find(']')] + '] '
newAlias = alias + '[' + ','.join([arg['alias'] for arg in args]) + ']'

View File

@ -12,8 +12,6 @@ from .basics import (
WordFormSerializer
)
from .data_access import (
AttributionCreateSerializer,
AttributionDataSerializer,
CrucialUpdateSerializer,
CstCreateSerializer,
CstInfoSerializer,

View File

@ -133,7 +133,7 @@ class ReferenceSerializer(StrictSerializer):
class InheritanceDataSerializer(StrictSerializer):
''' Serializer: Inheritance data. '''
''' Serializer: inheritance data. '''
child = serializers.IntegerField()
child_source = serializers.IntegerField()
parent = serializers.IntegerField() # type: ignore

View File

@ -17,55 +17,11 @@ from apps.oss.models import Inheritance
from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer
from ..models import Attribution, Constituenta, CstType, RSForm
from ..models import Constituenta, CstType, RSForm
from .basics import CstParseSerializer, InheritanceDataSerializer
from .io_pyconcept import PyConceptAdapter
class AttributionSerializer(StrictModelSerializer):
''' Serializer: Attribution relation. '''
class Meta:
''' serializer metadata. '''
model = Attribution
fields = ('container', 'attribute')
class AttributionDataSerializer(StrictSerializer):
''' Serializer: Attribution data. '''
container = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
attribute = PKField(many=False, queryset=Constituenta.objects.all().only('schema_id'))
def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema'])
if schema and attrs['container'].schema_id != schema.id:
raise serializers.ValidationError({
'container': msg.constituentaNotInRSform(schema.title)
})
if schema and attrs['attribute'].schema_id != schema.id:
raise serializers.ValidationError({
'attribute': msg.constituentaNotInRSform(schema.title)
})
return attrs
class AttributionCreateSerializer(AttributionDataSerializer):
''' Serializer: Data for creating new Attribution. '''
def validate(self, attrs):
attrs = super().validate(attrs)
if attrs['container'].pk == attrs['attribute'].pk:
raise serializers.ValidationError({
'container': msg.associationSelf()
})
if Attribution.objects.filter(container=attrs['container'], attribute=attrs['attribute']).exists():
raise serializers.ValidationError({
'attribute': msg.associationAlreadyExists()
})
return attrs
class CstBaseSerializer(StrictModelSerializer):
''' Serializer: Constituenta all data. '''
class Meta:
@ -166,15 +122,6 @@ class CstCreateSerializer(StrictModelSerializer):
'term_raw', 'definition_raw', 'definition_formal', \
'insert_after', 'term_forms'
def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema'])
insert_after = attrs.get('insert_after')
if insert_after and insert_after.schema_id != schema.pk:
raise serializers.ValidationError({
'insert_after': msg.constituentaNotInRSform(schema.title)
})
return attrs
class RSFormSerializer(StrictModelSerializer):
''' Serializer: Detailed data for RSForm. '''
@ -187,9 +134,6 @@ class RSFormSerializer(StrictModelSerializer):
inheritance = serializers.ListField(
child=InheritanceDataSerializer()
)
attribution = serializers.ListField(
child=AttributionSerializer()
)
oss = serializers.ListField(
child=LibraryItemReferenceSerializer()
)
@ -220,7 +164,6 @@ class RSFormSerializer(StrictModelSerializer):
result['items'] = []
result['oss'] = []
result['inheritance'] = []
result['attribution'] = []
for cst in Constituenta.objects.filter(schema=instance).defer('order').order_by('order'):
result['items'].append(CstInfoSerializer(cst).data)
for oss in LibraryItem.objects.filter(operations__result=instance).only('alias'):
@ -228,11 +171,6 @@ class RSFormSerializer(StrictModelSerializer):
'id': oss.pk,
'alias': oss.alias
})
for assoc in Attribution.objects.filter(container__schema=instance).only('container_id', 'attribute_id'):
result['attribution'].append({
'container': assoc.container_id,
'attribute': assoc.attribute_id
})
return result
def to_versioned_data(self) -> dict:
@ -262,38 +200,37 @@ class RSFormSerializer(StrictModelSerializer):
instance = cast(LibraryItem, self.instance)
schema = RSForm(instance)
items: list[dict] = data['items']
stored_ids: list[int] = [item['id'] for item in items]
id_map: dict[int, int] = {}
ids: list[int] = [item['id'] for item in items]
processed: list[int] = []
for existing_cst in schema.constituentsQ():
if not existing_cst.pk in stored_ids:
existing_cst.delete()
for cst in schema.constituentsQ():
if not cst.pk in ids:
cst.delete()
else:
cst_data = next(x for x in items if x['id'] == existing_cst.pk)
cst_data = next(x for x in items if x['id'] == cst.pk)
cst_data['schema'] = instance.pk
cst_serializer = CstBaseSerializer(data=cst_data)
cst_serializer.is_valid(raise_exception=True)
cst_serializer.validated_data['order'] = stored_ids.index(existing_cst.pk)
cst_serializer.update(
instance=existing_cst,
validated_data=cst_serializer.validated_data
new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(cst.pk)
new_cst.update(
instance=cst,
validated_data=new_cst.validated_data
)
id_map[cst_data['id']] = existing_cst.pk
processed.append(cst.pk)
for cst_data in items:
if cst_data['id'] not in id_map:
if cst_data['id'] not in processed:
cst = schema.insert_last(cst_data['alias'])
old_id = cst_data['id']
inserted_cst = schema.insert_last(cst_data['alias'])
cst_data['id'] = inserted_cst.pk
cst_data['id'] = cst.pk
cst_data['schema'] = instance.pk
cst_serializer = CstBaseSerializer(data=cst_data)
cst_serializer.is_valid(raise_exception=True)
cst_serializer.validated_data['order'] = stored_ids.index(old_id)
cst_serializer.update(
instance=inserted_cst,
validated_data=cst_serializer.validated_data
new_cst = CstBaseSerializer(data=cst_data)
new_cst.is_valid(raise_exception=True)
new_cst.validated_data['order'] = ids.index(old_id)
new_cst.update(
instance=cst,
validated_data=new_cst.validated_data
)
id_map[old_id] = inserted_cst.pk
loaded_item = LibraryItemBaseNonStrictSerializer(data=data)
loaded_item.is_valid(raise_exception=True)
@ -302,23 +239,6 @@ class RSFormSerializer(StrictModelSerializer):
validated_data=loaded_item.validated_data
)
Attribution.objects.filter(container__schema=instance).delete()
attributions_to_create: list[Attribution] = []
for assoc in data.get('attribution', []):
old_container_id = assoc['container']
old_attribute_id = assoc['attribute']
container_id = id_map.get(old_container_id)
attribute_id = id_map.get(old_attribute_id)
if container_id and attribute_id:
attributions_to_create.append(
Attribution(
container_id=container_id,
attribute_id=attribute_id
)
)
if attributions_to_create:
Attribution.objects.bulk_create(attributions_to_create)
class RSFormParseSerializer(StrictModelSerializer):
''' Serializer: Detailed data for RSForm including parse. '''
@ -347,8 +267,6 @@ class RSFormParseSerializer(StrictModelSerializer):
def _parse_data(self, data: dict) -> dict:
parse = PyConceptAdapter(data).parse()
for cst_data in data['items']:
if cst_data['cst_type'] == CstType.NOMINAL:
continue
cst_data['parse'] = next(
cst['parse'] for cst in parse['items']
if cst['id'] == cst_data['id']

View File

@ -6,11 +6,11 @@ from apps.library.models import LibraryItem
from shared import messages as msg
from shared.serializers import StrictSerializer
from ..models import Constituenta, CstType, RSFormCached
from ..models import Constituenta, RSFormCached
from ..utils import fix_old_references
_ENTITY_CONSTITUENTA = 'constituenta'
_ENTITY_SCHEMA = 'rsform'
_CST_TYPE = 'constituenta'
_TRS_TYPE = 'rsform'
_TRS_VERSION_MIN = 16
_TRS_VERSION = 16
_TRS_HEADER = 'Exteor 4.8.13.1000 - 30/05/2022'
@ -30,11 +30,11 @@ class RSFormUploadSerializer(StrictSerializer):
def generate_trs(schema: LibraryItem) -> dict:
''' Generate TRS file for RSForm. '''
items = []
for cst in Constituenta.objects.filter(schema=schema).exclude(cst_type=CstType.NOMINAL).order_by('order'):
for cst in Constituenta.objects.filter(schema=schema).order_by('order'):
items.append(
{
'entityUID': cst.pk,
'type': _ENTITY_CONSTITUENTA,
'type': _CST_TYPE,
'cstType': cst.cst_type,
'alias': cst.alias,
'convention': cst.convention,
@ -53,7 +53,7 @@ def generate_trs(schema: LibraryItem) -> dict:
}
)
return {
'type': _ENTITY_SCHEMA,
'type': _TRS_TYPE,
'title': schema.title,
'alias': schema.alias,
'comment': schema.description,
@ -72,7 +72,7 @@ class RSFormTRSSerializer(serializers.Serializer):
def load_versioned_data(data: dict) -> dict:
''' Load data from version. '''
result = {
'type': _ENTITY_SCHEMA,
'type': _TRS_TYPE,
'title': data['title'],
'alias': data['alias'],
'comment': data['description'],
@ -85,7 +85,7 @@ class RSFormTRSSerializer(serializers.Serializer):
for cst in data['items']:
result['items'].append({
'entityUID': cst['id'],
'type': _ENTITY_CONSTITUENTA,
'type': _CST_TYPE,
'cstType': cst['cst_type'],
'alias': cst['alias'],
'convention': cst['convention'],

View File

@ -6,7 +6,7 @@ import pyconcept
from shared import messages as msg
from ..models import Constituenta, CstType
from ..models import Constituenta
class PyConceptAdapter:
@ -34,7 +34,7 @@ class PyConceptAdapter:
result: dict = {
'items': []
}
items = Constituenta.objects.filter(schema_id=schemaID).exclude(cst_type=CstType.NOMINAL).order_by('order')
items = Constituenta.objects.filter(schema_id=schemaID).order_by('order')
for cst in items:
result['items'].append({
'entityUID': cst.pk,
@ -51,8 +51,6 @@ class PyConceptAdapter:
'items': []
}
for cst in data['items']:
if cst['cst_type'] == CstType.NOMINAL:
continue
result['items'].append({
'entityUID': cst['id'],
'cstType': cst['cst_type'],

View File

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

View File

@ -1,175 +0,0 @@
''' Testing models: Attribution. '''
from django.db.utils import IntegrityError
from django.test import TestCase
from apps.rsform.models import Attribution, Constituenta, CstType, RSForm
class TestAttribution(TestCase):
''' Testing Attribution model. '''
def setUp(self):
self.schema = RSForm.create(title='Test1')
# Create test constituents
self.container1 = Constituenta.objects.create(
alias='C1',
schema=self.schema.model,
order=1,
cst_type=CstType.NOMINAL
)
self.attribute1 = Constituenta.objects.create(
alias='A1',
schema=self.schema.model,
order=2,
cst_type=CstType.BASE
)
def test_str(self):
''' Test string representation. '''
attribution = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
expected_str = f'{self.container1} -> {self.attribute1}'
self.assertEqual(str(attribution), expected_str)
def test_create_attribution(self):
''' Test basic Attribution creation. '''
attr = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
self.assertEqual(attr.container, self.container1)
self.assertEqual(attr.attribute, self.attribute1)
self.assertIsNotNone(attr.id)
def test_unique_constraint(self):
''' Test unique constraint on container and attribute. '''
# Create first Attribution
Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
# Try to create duplicate Attribution
with self.assertRaises(IntegrityError):
Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
def test_container_not_null(self):
''' Test container field cannot be null. '''
with self.assertRaises(IntegrityError):
Attribution.objects.create(
container=None,
attribute=self.attribute1
)
def test_attribute_not_null(self):
''' Test attribute field cannot be null. '''
with self.assertRaises(IntegrityError):
Attribution.objects.create(
container=self.container1,
attribute=None
)
def test_cascade_delete_container(self):
''' Test cascade delete when container is deleted. '''
attribution = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
association_id = attribution.id
# Delete the container
self.container1.delete()
# Attribution should be deleted due to CASCADE
with self.assertRaises(Attribution.DoesNotExist):
Attribution.objects.get(id=association_id)
def test_cascade_delete_attribute(self):
''' Test cascade delete when attribute is deleted. '''
attribution = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
association_id = attribution.id
# Delete the attribute
self.attribute1.delete()
# Attribution should be deleted due to CASCADE
with self.assertRaises(Attribution.DoesNotExist):
Attribution.objects.get(id=association_id)
def test_related_names(self):
''' Test related names for foreign key relationships. '''
attribution = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
# Test container related name
container_associations = self.container1.as_container.all()
self.assertEqual(len(container_associations), 1)
self.assertEqual(container_associations[0], attribution)
# Test attribute related name
attribute_associations = self.attribute1.as_attribute.all()
self.assertEqual(len(attribute_associations), 1)
self.assertEqual(attribute_associations[0], attribution)
def test_multiple_attributions_same_container(self):
''' Test multiple Attributions with same container. '''
attribute3 = Constituenta.objects.create(
alias='A3',
schema=self.schema.model,
order=3,
cst_type=CstType.BASE
)
attr1 = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
attr2 = Attribution.objects.create(
container=self.container1,
attribute=attribute3
)
container_associations = self.container1.as_container.all()
self.assertEqual(len(container_associations), 2)
self.assertIn(attr1, container_associations)
self.assertIn(attr2, container_associations)
def test_multiple_attributions_same_attribute(self):
''' Test multiple Attributions with same attribute. '''
container3 = Constituenta.objects.create(
alias='C3',
schema=self.schema.model,
order=3,
cst_type=CstType.NOMINAL
)
attr1 = Attribution.objects.create(
container=self.container1,
attribute=self.attribute1
)
attr2 = Attribution.objects.create(
container=container3,
attribute=self.attribute1
)
attribute_associations = self.attribute1.as_attribute.all()
self.assertEqual(len(attribute_associations), 2)
self.assertIn(attr1, attribute_associations)
self.assertIn(attr2, attribute_associations)
def test_meta_unique_together(self):
''' Test Meta class unique_together constraint. '''
unique_together = Attribution._meta.unique_together
self.assertEqual(len(unique_together), 1)
self.assertIn(('container', 'attribute'), unique_together)

View File

@ -48,7 +48,7 @@ class TestRSFormCached(DBTester):
x1 = self.schema.insert_last('X1')
x2 = self.schema.insert_last('X2')
x3 = self.schema.create_cst(data, insert_after=x1)
x3 = self.schema.create_cst(data=data, insert_after=x1)
x2.refresh_from_db()
self.assertEqual(x3.alias, data['alias'])

View File

@ -1,6 +1,4 @@
''' Tests for REST API. '''
from .t_attribtuions import *
from .t_cctext import *
from .t_constituenta import *
from .t_rsforms import *
from .t_rslang import *

View File

@ -1,100 +0,0 @@
''' Testing API: Attribution. '''
import io
import os
from zipfile import ZipFile
from cctext import ReferenceType
from rest_framework import status
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
from apps.rsform.models import Attribution, Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
from shared.testing_utils import response_contains
class TestAttributionsEndpoints(EndpointTester):
''' Testing basic Attribution API. '''
def setUp(self):
super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.n1 = self.owned.insert_last('N1')
self.x1 = self.owned.insert_last('X1')
self.n2 = self.owned.insert_last('N2')
self.unowned_cst = self.unowned.insert_last('C1')
self.invalid_id = self.n2.pk + 1337
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
def test_create_attribution(self):
self.executeBadData({}, item=self.owned_id)
data = {'container': self.n1.pk, 'attribute': self.invalid_id}
self.executeBadData(data, item=self.owned_id)
data['attribute'] = self.unowned_cst.pk
self.executeBadData(data, item=self.owned_id)
data['attribute'] = data['container']
self.executeBadData(data, item=self.owned_id)
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
self.executeBadData(data, item=self.unowned_id)
response = self.executeCreated(data, item=self.owned_id)
associations = response.data['attribution']
self.assertEqual(len(associations), 1)
self.assertEqual(associations[0]['container'], self.n1.pk)
self.assertEqual(associations[0]['attribute'], self.x1.pk)
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
def test_create_attribution_duplicate(self):
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
self.executeCreated(data, item=self.owned_id)
self.executeBadData(data, item=self.owned_id)
@decl_endpoint('/api/rsforms/{item}/delete-attribution', method='patch')
def test_delete_attribution(self):
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
self.executeForbidden(data, item=self.unowned_id)
self.executeBadData(data, item=self.owned_id)
Attribution.objects.create(
container=self.n1,
attribute=self.x1
)
self.executeForbidden(data, item=self.unowned_id)
response = self.executeOK(data, item=self.owned_id)
attributions = response.data['attribution']
self.assertEqual(len(attributions), 0)
@decl_endpoint('/api/rsforms/{item}/clear-attributions', method='patch')
def test_clear_attributions(self):
data = {'target': self.n1.pk}
self.executeForbidden(data, item=self.unowned_id)
self.executeNotFound(data, item=self.invalid_id)
self.executeOK(data, item=self.owned_id)
Attribution.objects.create(
container=self.n1,
attribute=self.x1
)
Attribution.objects.create(
container=self.n1,
attribute=self.n2
)
Attribution.objects.create(
container=self.n2,
attribute=self.n1
)
response = self.executeOK(data, item=self.owned_id)
associations = response.data['attribution']
self.assertEqual(len(associations), 1)
self.assertEqual(associations[0]['container'], self.n2.pk)
self.assertEqual(associations[0]['attribute'], self.n1.pk)

View File

@ -14,20 +14,20 @@ class TestNaturalLanguageViews(EndpointTester):
@decl_endpoint(endpoint='/api/cctext/parse', method='post')
def test_parse_text(self):
data = {'text': 'синим слонам'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self._assert_tags(response.data['result'], 'datv,NOUN,plur,anim,masc')
@decl_endpoint(endpoint='/api/cctext/inflect', method='post')
def test_inflect(self):
data = {'text': 'синий слон', 'grams': 'plur,datv'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertEqual(response.data['result'], 'синим слонам')
@decl_endpoint(endpoint='/api/cctext/generate-lexeme', method='post')
def test_generate_lexeme(self):
data = {'text': 'синий слон'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertEqual(len(response.data['items']), 12)
self.assertEqual(response.data['items'][0]['text'], 'синий слон')

View File

@ -1,195 +0,0 @@
''' Testing API: Constituenta editing. '''
from apps.rsform.models import Constituenta, CstType, RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestConstituentaAPI(EndpointTester):
''' Testing Constituenta view. '''
def setUp(self):
super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.x1 = Constituenta.objects.create(
alias='X1',
cst_type=CstType.BASE,
schema=self.owned.model,
order=0,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
self.x2 = Constituenta.objects.create(
alias='X2',
cst_type=CstType.BASE,
schema=self.owned.model,
order=1,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.x3 = Constituenta.objects.create(
alias='X3',
schema=self.owned.model,
order=2,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
self.unowned_cst = self.unowned.insert_last(alias='X1', cst_type=CstType.BASE)
self.invalid_cst = self.x3.pk + 1337
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
def test_partial_update(self):
data = {'target': self.x1.pk, 'item_data': {'convention': 'tt'}}
self.executeForbidden(data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.login()
self.executeOK(data)
self.x1.refresh_from_db()
self.assertEqual(self.x1.convention, 'tt')
self.executeOK(data)
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
def test_partial_update_rename(self):
data = {'target': self.x1.pk, 'item_data': {'alias': self.x3.alias}}
self.executeBadData(data, item=self.owned_id)
d1 = self.owned.insert_last(
alias='D1',
term_raw='@{X1|plur}',
definition_formal='X1'
)
self.assertEqual(self.x1.order, 0)
self.assertEqual(self.x1.alias, 'X1')
self.assertEqual(self.x1.cst_type, CstType.BASE)
data = {'target': self.x1.pk, 'item_data': {'alias': 'D2', 'cst_type': CstType.TERM}}
self.executeOK(data, item=self.owned_id)
d1.refresh_from_db()
self.x1.refresh_from_db()
self.assertEqual(d1.term_resolved, '')
self.assertEqual(d1.term_raw, '@{D2|plur}')
self.assertEqual(self.x1.order, 0)
self.assertEqual(self.x1.alias, 'D2')
self.assertEqual(self.x1.cst_type, CstType.TERM)
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
def test_update_resolved_no_refs(self):
data = {
'target': self.x3.pk,
'item_data': {
'term_raw': 'New term',
'definition_raw': 'New def'
}
}
self.executeOK(data, item=self.owned_id)
self.x3.refresh_from_db()
self.assertEqual(self.x3.term_resolved, 'New term')
self.assertEqual(self.x3.definition_resolved, 'New def')
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
def test_update_resolved_refs(self):
data = {
'target': self.x3.pk,
'item_data': {
'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
}
}
self.executeOK(data, item=self.owned_id)
self.x3.refresh_from_db()
self.assertEqual(self.x3.term_resolved, self.x1.term_resolved)
self.assertEqual(self.x3.definition_resolved, f'{self.x1.term_resolved} form1')
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
def test_update_term_forms(self):
data = {
'target': self.x3.pk,
'item_data': {
'definition_raw': '@{X3|sing,datv}',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
}
}
self.executeOK(data, item=self.owned_id)
self.x3.refresh_from_db()
self.assertEqual(self.x3.definition_resolved, 'form1')
self.assertEqual(self.x3.term_forms, data['item_data']['term_forms'])
@decl_endpoint('/api/rsforms/{item}/update-crucial', method='patch')
def test_update_crucial(self):
data = {'target': [self.x1.pk], 'value': True}
self.executeForbidden(data, item=self.unowned_id)
self.logout()
self.executeForbidden(data, item=self.owned_id)
self.login()
self.executeOK(data, item=self.owned_id)
self.x1.refresh_from_db()
self.assertEqual(self.x1.crucial, True)
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
def test_create_constituenta(self):
data = {'alias': 'X4', 'cst_type': CstType.BASE}
self.executeForbidden(data, item=self.unowned_id)
data = {'alias': 'X4'}
self.executeBadData(item=self.owned_id)
self.executeBadData(data)
data['cst_type'] = 'invalid'
self.executeBadData(data)
data = {
'alias': 'X4',
'cst_type': CstType.BASE,
'term_raw': 'test',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
'definition_formal': 'invalid',
'crucial': True
}
response = self.executeCreated(data)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 3)
self.assertEqual(x4.term_raw, data['term_raw'])
self.assertEqual(x4.term_forms, data['term_forms'])
self.assertEqual(x4.definition_formal, data['definition_formal'])
self.assertEqual(x4.crucial, data['crucial'])
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
def test_create_constituenta_after(self):
self.set_params(item=self.owned_id)
data = {'alias': 'X4', 'cst_type': CstType.BASE, 'insert_after': self.invalid_cst}
self.executeBadData(data)
data['insert_after'] = self.unowned_cst.pk
self.executeBadData(data)
data = {
'alias': 'X4',
'cst_type': CstType.BASE,
'insert_after': self.x2.pk,
}
response = self.executeCreated(data)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.x3.refresh_from_db()
self.assertEqual(x4.order, 2)
self.assertEqual(self.x3.order, 3)

View File

@ -36,11 +36,11 @@ class TestRSFormViewset(EndpointTester):
'access_policy': AccessPolicy.PROTECTED,
'visible': False
}
self.executeBadData(data)
self.executeBadData(data=data)
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data['file'] = file
response = self.client.post(self.endpoint, data, format='multipart')
response = self.client.post(self.endpoint, data=data, format='multipart')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertEqual(response.data['title'], data['title'])
@ -117,21 +117,21 @@ class TestRSFormViewset(EndpointTester):
def test_check_expression(self):
self.owned.insert_last('X1')
data = {'expression': 'X1=X1'}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], 'math')
self.assertEqual(response.data['astText'], '[=[X1][X1]]')
self.assertEqual(response.data['typification'], 'LOGIC')
self.assertEqual(response.data['valueClass'], 'value')
self.executeOK(data, item=self.unowned_id)
self.executeOK(data=data, item=self.unowned_id)
@decl_endpoint('/api/rsforms/{item}/check-constituenta', method='post')
def test_check_constituenta(self):
self.owned.insert_last('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'A111', 'cst_type': CstType.AXIOM}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], 'math')
self.assertEqual(response.data['astText'], '[:==[A111][=[X1][X1]]]')
@ -143,7 +143,7 @@ class TestRSFormViewset(EndpointTester):
def test_check_constituenta_error(self):
self.owned.insert_last('X1')
data = {'definition_formal': 'X1=X1', 'alias': 'D111', 'cst_type': CstType.TERM}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['parseResult'], False)
@ -155,7 +155,7 @@ class TestRSFormViewset(EndpointTester):
)
data = {'text': '@{1|редкий} @{X1|plur,datv}'}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
self.assertEqual(response.data['input'], '@{1|редкий} @{X1|plur,datv}')
self.assertEqual(response.data['output'], 'редким синим слонам')
self.assertEqual(len(response.data['refs']), 2)
@ -182,7 +182,7 @@ class TestRSFormViewset(EndpointTester):
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file}
response = self.client.post(self.endpoint, data, format='multipart')
response = self.client.post(self.endpoint, data=data, format='multipart')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['owner'], self.user.pk)
self.assertTrue(response.data['title'] != '')
@ -200,10 +200,57 @@ class TestRSFormViewset(EndpointTester):
self.assertIn('document.json', zipped_file.namelist())
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
def test_create_constituenta(self):
data = {'alias': 'X3', 'cst_type': CstType.BASE}
self.executeForbidden(data=data, item=self.unowned_id)
data = {'alias': 'X3'}
self.owned.insert_last('X1')
x2 = self.owned.insert_last('X2')
self.executeBadData(item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
data['cst_type'] = 'invalid'
self.executeBadData(data=data, item=self.owned_id)
data['cst_type'] = CstType.BASE
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
x3 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x3.order, 2)
data = {
'alias': 'X4',
'cst_type': CstType.BASE,
'insert_after': x2.pk,
'term_raw': 'test',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}],
'definition_formal': 'invalid',
'crucial': True
}
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 2)
self.assertEqual(x4.term_raw, data['term_raw'])
self.assertEqual(x4.term_forms, data['term_forms'])
self.assertEqual(x4.definition_formal, data['definition_formal'])
self.assertEqual(x4.crucial, data['crucial'])
data = {
'alias': 'X5',
'cst_type': CstType.BASE,
'insert_after': None,
'term_raw': 'test5'
}
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], data['alias'])
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
def test_substitute_multiple(self):
self.set_params(item=self.owned_id)
x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_last('X2')
d1 = self.owned.insert_last('D1')
@ -214,7 +261,7 @@ class TestRSFormViewset(EndpointTester):
)
data = {'substitutions': []}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'substitutions': [
{
@ -226,7 +273,7 @@ class TestRSFormViewset(EndpointTester):
'substitution': d2.pk
}
]}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'substitutions': [
{
@ -238,7 +285,7 @@ class TestRSFormViewset(EndpointTester):
'substitution': d2.pk
}
]}
response = self.executeOK(data, item=self.owned_id)
response = self.executeOK(data=data, item=self.owned_id)
d3.refresh_from_db()
self.assertEqual(d3.definition_formal, r'D1 \ D2')
@ -253,7 +300,7 @@ class TestRSFormViewset(EndpointTester):
'definition_formal': '3',
'definition_raw': '4'
}
response = self.executeCreated(data, item=self.owned_id)
response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
self.assertEqual(response.data['new_cst']['cst_type'], CstType.BASE)
self.assertEqual(response.data['new_cst']['convention'], '1')
@ -269,13 +316,13 @@ class TestRSFormViewset(EndpointTester):
self.set_params(item=self.owned_id)
data = {'items': [1337]}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data)
x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_last('X2')
data = {'items': [x1.pk]}
response = self.executeOK(data)
response = self.executeOK(data=data)
x2.refresh_from_db()
self.owned.model.refresh_from_db()
self.assertEqual(len(response.data['items']), 1)
@ -285,7 +332,7 @@ class TestRSFormViewset(EndpointTester):
x3 = self.unowned.insert_last('X1')
data = {'items': [x3.pk]}
self.executeBadData(data, item=self.owned_id)
self.executeBadData(data=data, item=self.owned_id)
@decl_endpoint('/api/rsforms/{item}/move-cst', method='patch')
@ -293,13 +340,13 @@ class TestRSFormViewset(EndpointTester):
self.set_params(item=self.owned_id)
data = {'items': [1337], 'move_to': 0}
self.executeBadData(data)
self.executeBadData(data=data)
x1 = self.owned.insert_last('X1')
x2 = self.owned.insert_last('X2')
data = {'items': [x2.pk], 'move_to': 0}
response = self.executeOK(data)
response = self.executeOK(data=data)
x1.refresh_from_db()
x2.refresh_from_db()
self.assertEqual(response.data['id'], self.owned_id)
@ -308,7 +355,7 @@ class TestRSFormViewset(EndpointTester):
x3 = self.unowned.insert_last('X1')
data = {'items': [x3.pk], 'move_to': 0}
self.executeBadData(data)
self.executeBadData(data=data)
@decl_endpoint('/api/rsforms/{item}/reset-aliases', method='patch')
@ -345,7 +392,7 @@ class TestRSFormViewset(EndpointTester):
work_dir = os.path.dirname(os.path.abspath(__file__))
with open(f'{work_dir}/data/sample-rsform.trs', 'rb') as file:
data = {'file': file, 'load_metadata': False}
response = self.client.patch(self.endpoint, data, format='multipart')
response = self.client.patch(self.endpoint, data=data, format='multipart')
self.owned.model.refresh_from_db()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.owned.model.title, 'Test11')
@ -385,7 +432,7 @@ class TestRSFormViewset(EndpointTester):
self.executeBadData({'target': s2.pk})
# Testing simple structure
response = self.executeOK({'target': s1.pk})
response = self.executeOK(data={'target': s1.pk})
result = response.data['schema']
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
self.assertEqual(len(items), 2)
@ -394,7 +441,7 @@ class TestRSFormViewset(EndpointTester):
# Testing complex structure
s3.refresh_from_db()
response = self.executeOK({'target': s3.pk})
response = self.executeOK(data={'target': s3.pk})
result = response.data['schema']
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
self.assertEqual(len(items), 8)
@ -402,15 +449,151 @@ class TestRSFormViewset(EndpointTester):
# Testing function
f1.refresh_from_db()
response = self.executeOK({'target': f1.pk})
response = self.executeOK(data={'target': f1.pk})
result = response.data['schema']
items = [item for item in result['items'] if item['id'] in response.data['cst_list']]
self.assertEqual(len(items), 2)
self.assertEqual(items[0]['definition_formal'], '[α∈X1, β∈X1] Pr1(F10[α,β])')
class TestConstituentaAPI(EndpointTester):
''' Testing Constituenta view. '''
def setUp(self):
super().setUp()
self.owned = RSForm.create(title='Test', alias='T1', owner=self.user)
self.owned_id = self.owned.model.pk
self.unowned = RSForm.create(title='Test2', alias='T2')
self.unowned_id = self.unowned.model.pk
self.cst1 = Constituenta.objects.create(
alias='X1',
cst_type=CstType.BASE,
schema=self.owned.model,
order=0,
convention='Test',
term_raw='Test1',
term_resolved='Test1R',
term_forms=[{'text': 'form1', 'tags': 'sing,datv'}])
self.cst2 = Constituenta.objects.create(
alias='X2',
cst_type=CstType.BASE,
schema=self.unowned.model,
order=0,
convention='Test1',
term_raw='Test2',
term_resolved='Test2R'
)
self.cst3 = Constituenta.objects.create(
alias='X3',
schema=self.owned.model,
order=1,
term_raw='Test3',
term_resolved='Test3',
definition_raw='Test1',
definition_resolved='Test2'
)
self.invalid_cst = self.cst3.pk + 1337
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_partial_update(self):
data = {'target': self.cst1.pk, 'item_data': {'convention': 'tt'}}
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.convention, 'tt')
self.executeOK(data=data, schema=self.owned_id)
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_partial_update_rename(self):
data = {'target': self.cst1.pk, 'item_data': {'alias': self.cst3.alias}}
self.executeBadData(data=data, schema=self.owned_id)
d1 = self.owned.insert_last(
alias='D1',
term_raw='@{X1|plur}',
definition_formal='X1'
)
self.assertEqual(self.cst1.order, 0)
self.assertEqual(self.cst1.alias, 'X1')
self.assertEqual(self.cst1.cst_type, CstType.BASE)
data = {'target': self.cst1.pk, 'item_data': {'alias': 'D2', 'cst_type': CstType.TERM}}
self.executeOK(data=data, schema=self.owned_id)
d1.refresh_from_db()
self.cst1.refresh_from_db()
self.assertEqual(d1.term_resolved, '')
self.assertEqual(d1.term_raw, '@{D2|plur}')
self.assertEqual(self.cst1.order, 0)
self.assertEqual(self.cst1.alias, 'D2')
self.assertEqual(self.cst1.cst_type, CstType.TERM)
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_no_refs(self):
data = {
'target': self.cst3.pk,
'item_data': {
'term_raw': 'New term',
'definition_raw': 'New def'
}
}
self.executeOK(data=data, schema=self.owned_id)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, 'New term')
self.assertEqual(self.cst3.definition_resolved, 'New def')
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_resolved_refs(self):
data = {
'target': self.cst3.pk,
'item_data': {
'term_raw': '@{X1|nomn,sing}',
'definition_raw': '@{X1|nomn,sing} @{X1|sing,datv}'
}
}
self.executeOK(data=data, schema=self.owned_id)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.term_resolved, self.cst1.term_resolved)
self.assertEqual(self.cst3.definition_resolved, f'{self.cst1.term_resolved} form1')
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
def test_update_term_forms(self):
data = {
'target': self.cst3.pk,
'item_data': {
'definition_raw': '@{X3|sing,datv}',
'term_forms': [{'text': 'form1', 'tags': 'sing,datv'}]
}
}
self.executeOK(data=data, schema=self.owned_id)
self.cst3.refresh_from_db()
self.assertEqual(self.cst3.definition_resolved, 'form1')
self.assertEqual(self.cst3.term_forms, data['item_data']['term_forms'])
@decl_endpoint('/api/rsforms/{schema}/update-crucial', method='patch')
def test_update_crucial(self):
data = {'target': [self.cst1.pk], 'value': True}
self.executeForbidden(data=data, schema=self.unowned_id)
self.logout()
self.executeForbidden(data=data, schema=self.owned_id)
self.login()
self.executeOK(data=data, schema=self.owned_id)
self.cst1.refresh_from_db()
self.assertEqual(self.cst1.crucial, True)
class TestInlineSynthesis(EndpointTester):
''' Testing Inline synthesis. '''
''' Testing Operations endpoints. '''
@decl_endpoint('/api/rsforms/inline-synthesis', method='patch')
@ -429,20 +612,20 @@ class TestInlineSynthesis(EndpointTester):
'items': [],
'substitutions': []
}
self.executeForbidden(data)
self.executeForbidden(data=data)
data['receiver'] = invalid_id
self.executeBadData(data)
self.executeBadData(data=data)
data['receiver'] = self.schema1.model.pk
data['source'] = invalid_id
self.executeBadData(data)
self.executeBadData(data=data)
data['source'] = self.schema1.model.pk
self.executeOK(data)
self.executeOK(data=data)
data['items'] = [invalid_id]
self.executeBadData(data)
self.executeBadData(data=data)
def test_inline_synthesis(self):
@ -471,7 +654,7 @@ class TestInlineSynthesis(EndpointTester):
}
]
}
response = self.executeOK(data)
response = self.executeOK(data=data)
result = {item['alias']: item for item in response.data['items']}
self.assertEqual(len(result), 6)
self.assertEqual(result['S1']['definition_formal'], 'X2')

View File

@ -8,30 +8,30 @@ class TestRSLanguageViews(EndpointTester):
@decl_endpoint('/api/rslang/to-ascii', method='post')
def test_convert_to_ascii(self):
data = {'data': '1=1'}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'expression': '1=1'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertEqual(response.data['result'], r'1 \eq 1')
@decl_endpoint('/api/rslang/to-math', method='post')
def test_convert_to_math(self):
data = {'data': r'1 \eq 1'}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'expression': r'1 \eq 1'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertEqual(response.data['result'], r'1=1')
@decl_endpoint('/api/rslang/parse-expression', method='post')
def test_parse_expression(self):
data = {'data': r'1=1'}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'expression': r'1=1'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertEqual(response.data['parseResult'], True)
self.assertEqual(response.data['syntax'], 'math')
self.assertEqual(response.data['astText'], '[=[1][1]]')

View File

@ -49,9 +49,6 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
'restore_order',
'reset_aliases',
'produce_structure',
'add_attribution',
'delete_attribution',
'clear_attributions'
]:
permission_list = [permissions.ItemEditor]
elif self.action in [
@ -82,7 +79,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def create_cst(self, request: Request, pk) -> HttpResponse:
''' Create Constituenta. '''
item = self._get_item()
serializer = s.CstCreateSerializer(data=request.data, context={'schema': item})
serializer = s.CstCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
if 'insert_after' not in data:
@ -235,7 +232,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def substitute(self, request: Request, pk) -> HttpResponse:
''' Substitute occurrences of constituenta with another one. '''
item = self._get_item()
serializer = s.CstSubstituteSerializer(data=request.data, context={'schema': item})
serializer = s.CstSubstituteSerializer(
data=request.data,
context={'schema': item}
)
serializer.is_valid(raise_exception=True)
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
@ -269,7 +269,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def delete_multiple_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete multiple Constituents. '''
item = self._get_item()
serializer = s.CstListSerializer(data=request.data, context={'schema': item})
serializer = s.CstListSerializer(
data=request.data,
context={'schema': item}
)
serializer.is_valid(raise_exception=True)
cst_list: list[m.Constituenta] = serializer.validated_data['items']
@ -284,106 +287,6 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
data=s.RSFormParseSerializer(item).data
)
@extend_schema(
summary='create Attribution',
tags=['Constituenta'],
request=s.AttributionCreateSerializer,
responses={
c.HTTP_201_CREATED: s.RSFormParseSerializer,
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-attribution')
def create_attribution(self, request: Request, pk) -> HttpResponse:
''' Create Attribution. '''
item = self._get_item()
serializer = s.AttributionCreateSerializer(data=request.data, context={'schema': item})
serializer.is_valid(raise_exception=True)
container = serializer.validated_data['container']
attribute = serializer.validated_data['attribute']
with transaction.atomic():
new_association = m.Attribution.objects.create(
container=container,
attribute=attribute
)
PropagationFacade.after_create_attribution(item.pk, [new_association])
item.save(update_fields=['time_update'])
return Response(
status=c.HTTP_201_CREATED,
data=s.RSFormParseSerializer(item).data
)
@extend_schema(
summary='delete Association',
tags=['RSForm'],
request=s.AttributionDataSerializer,
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='delete-attribution')
def delete_attribution(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Attribution. '''
item = self._get_item()
serializer = s.AttributionDataSerializer(data=request.data, context={'schema': item})
serializer.is_valid(raise_exception=True)
with transaction.atomic():
target = list(m.Attribution.objects.filter(
container=serializer.validated_data['container'],
attribute=serializer.validated_data['attribute']
))
if not target:
raise ValidationError({
'container': msg.invalidAssociation()
})
PropagationFacade.before_delete_attribution(item.pk, target)
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
item.save(update_fields=['time_update'])
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(item).data
)
@extend_schema(
summary='delete all Attributions for target constituenta',
tags=['RSForm'],
request=s.CstTargetSerializer,
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='clear-attributions')
def clear_attributions(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Delete Associations for target Constituenta. '''
item = self._get_item()
serializer = s.CstTargetSerializer(data=request.data, context={'schema': item})
serializer.is_valid(raise_exception=True)
with transaction.atomic():
target = list(m.Attribution.objects.filter(container=serializer.validated_data['target']))
if target:
PropagationFacade.before_delete_attribution(item.pk, target)
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
item.save(update_fields=['time_update'])
return Response(
status=c.HTTP_200_OK,
data=s.RSFormParseSerializer(item).data
)
@extend_schema(
summary='move constituenta',
tags=['RSForm'],
@ -399,7 +302,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
def move_cst(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Move multiple Constituents. '''
item = self._get_item()
serializer = s.CstMoveSerializer(data=request.data, context={'schema': item})
serializer = s.CstMoveSerializer(
data=request.data,
context={'schema': item}
)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
@ -491,7 +397,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
)
data['id'] = item.pk
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': load_metadata})
serializer = s.RSFormTRSSerializer(
data=data,
context={'load_meta': load_metadata}
)
serializer.is_valid(raise_exception=True)
result: m.RSForm = serializer.save()
return Response(
@ -649,7 +558,10 @@ class TrsImportView(views.APIView):
)
owner = cast(User, self.request.user)
_prepare_rsform_data(data, request, owner)
serializer = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
serializer = s.RSFormTRSSerializer(
data=data,
context={'load_meta': True}
)
serializer.is_valid(raise_exception=True)
schema: m.RSForm = serializer.save()
return Response(
@ -728,12 +640,15 @@ def _prepare_rsform_data(data: dict, request: Request, owner: Union[User, None])
@api_view(['PATCH'])
def inline_synthesis(request: Request) -> HttpResponse:
''' Endpoint: Inline synthesis. '''
serializer = s.InlineSynthesisSerializer(data=request.data, context={'user': request.user})
serializer = s.InlineSynthesisSerializer(
data=request.data,
context={'user': request.user}
)
serializer.is_valid(raise_exception=True)
receiver = m.RSFormCached(serializer.validated_data['receiver'])
items = cast(list[m.Constituenta], serializer.validated_data['items'])
if not items:
if len(items) == 0:
source = cast(LibraryItem, serializer.validated_data['source'])
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))

View File

@ -16,15 +16,15 @@ class TestUserAPIViews(EndpointTester):
def test_login(self):
self.logout()
data = {'username': self.user.username, 'password': 'invalid'}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'username': self.user.username, 'password': 'password'}
self.executeAccepted(data)
self.executeAccepted(data)
self.executeAccepted(data=data)
self.executeAccepted(data=data)
self.logout()
data = {'username': self.user.email, 'password': 'password'}
self.executeAccepted(data)
self.executeAccepted(data=data)
@decl_endpoint('/users/api/logout', method='post')
@ -82,7 +82,7 @@ class TestUserUserProfileAPIView(EndpointTester):
'first_name': 'firstName',
'last_name': 'lastName',
}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.user.refresh_from_db()
self.assertEqual(response.data['email'], '123@mail.ru')
self.assertEqual(self.user.email, '123@mail.ru')
@ -96,13 +96,13 @@ class TestUserUserProfileAPIView(EndpointTester):
'first_name': 'new',
'last_name': 'new2',
}
self.executeOK(data)
self.executeOK(data=data)
data = {'email': self.user2.email}
self.executeBadData(data)
self.executeBadData(data=data)
data = {'username': 'new_username'}
response = self.executeOK(data)
response = self.executeOK(data=data)
self.assertNotEqual(response.data['username'], data['username'])
self.logout()
@ -115,14 +115,14 @@ class TestUserUserProfileAPIView(EndpointTester):
'old_password': 'invalid',
'new_password': 'password2'
}
self.executeBadData(data)
self.executeBadData(data=data)
data = {
'old_password': 'password',
'new_password': 'password2'
}
oldHash = self.user.password
response = self.executeNoContent(data)
response = self.executeNoContent(data=data)
self.user.refresh_from_db()
self.assertNotEqual(self.user.password, oldHash)
self.assertTrue(self.client.login(username=self.user.username, password='password2'))
@ -155,7 +155,7 @@ class TestSignupAPIView(EndpointTester):
'first_name': 'firstName',
'last_name': 'lastName'
}
self.executeBadData(data)
self.executeBadData(data=data)
data = {
'username': 'NewUser',
@ -165,7 +165,7 @@ class TestSignupAPIView(EndpointTester):
'first_name': 'firstName',
'last_name': 'lastName'
}
response = self.executeCreated(data)
response = self.executeCreated(data=data)
self.assertTrue('id' in response.data)
self.assertEqual(response.data['username'], data['username'])
self.assertEqual(response.data['email'], data['email'])
@ -180,7 +180,7 @@ class TestSignupAPIView(EndpointTester):
'first_name': 'firstName',
'last_name': 'lastName'
}
self.executeBadData(data)
self.executeBadData(data=data)
data = {
'username': 'NewUser2',
@ -190,4 +190,4 @@ class TestSignupAPIView(EndpointTester):
'first_name': 'firstName',
'last_name': 'lastName'
}
self.executeBadData(data)
self.executeBadData(data=data)

View File

@ -10,14 +10,6 @@ def constituentsInvalid(constituents: list[int]):
return f'некорректные конституенты для схемы: {constituents}'
def associationSelf():
return 'Рефлексивная ассоциация не допускается'
def associationAlreadyExists():
return 'Отношение уже существует'
def constituentaNotInRSform(title: str):
return f'Конституента не принадлежит схеме: {title}'
@ -30,10 +22,6 @@ def operationNotInOSS():
return 'Операция не принадлежит ОСС'
def duplicateSchemasInArguments():
return 'Аргументы не должны содержать повторяющиеся КС'
def blockNotInOSS():
return 'Блок не принадлежит ОСС'
@ -98,12 +86,12 @@ def operationInputAlreadyConnected():
return 'Схема уже подключена к другой операции'
def replicaNotAllowed():
return 'Реплики не поддерживаются'
def referenceTypeNotAllowed():
return 'Ссылки не поддерживаются'
def replicaRequired():
return 'Операция должна быть репликацией'
def referenceTypeRequired():
return 'Операция должна быть ссылкой'
def operationNotSynthesis(title: str):
@ -150,10 +138,6 @@ def typificationInvalidStr():
return 'Invalid typification string'
def invalidAssociation():
return f'Ассоциация не найдена'
def exteorFileVersionNotSupported():
return 'Некорректный формат файла Экстеор. Сохраните файл в новой версии'

View File

@ -21,5 +21,5 @@ def write_zipped_json(json_data: dict, json_filename: str) -> bytes:
content = BytesIO()
data = json.dumps(json_data, indent=4, ensure_ascii=False)
with ZipFile(content, 'w') as archive:
archive.writestr(json_filename, data)
archive.writestr(json_filename, data=data)
return content.getvalue()

File diff suppressed because it is too large Load Diff

View File

@ -10,77 +10,77 @@
"dev": "vite --host",
"build": "tsc && vite build",
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 1 --fix",
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
"preview": "vite preview --port 3000"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.5",
"@hookform/resolvers": "^5.2.2",
"@hookform/resolvers": "^5.2.1",
"@lezer/lr": "^1.4.2",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3",
"@uiw/codemirror-themes": "^4.25.2",
"@uiw/react-codemirror": "^4.25.2",
"axios": "^1.12.2",
"@uiw/codemirror-themes": "^4.24.1",
"@uiw/react-codemirror": "^4.24.1",
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"global": "^4.4.0",
"js-file-download": "^0.4.12",
"lucide-react": "^0.545.0",
"lucide-react": "^0.533.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.65.0",
"react-hook-form": "^7.61.1",
"react-icons": "^5.5.0",
"react-intl": "^7.1.14",
"react-router": "^7.9.4",
"react-intl": "^7.1.11",
"react-router": "^7.7.1",
"react-scan": "^0.4.3",
"react-tabs": "^6.1.0",
"react-toastify": "^11.0.5",
"react-tooltip": "^5.30.0",
"react-tooltip": "^5.29.1",
"react-zoom-pan-pinch": "^3.7.0",
"reactflow": "^11.11.4",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.7",
"use-debounce": "^10.0.6",
"zod": "^4.1.12",
"zustand": "^5.0.8"
"tw-animate-css": "^1.3.6",
"use-debounce": "^10.0.5",
"zod": "^4.0.13",
"zustand": "^5.0.6"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"@playwright/test": "^1.56.0",
"@tailwindcss/vite": "^4.1.14",
"@playwright/test": "^1.54.1",
"@tailwindcss/vite": "^4.1.11",
"@types/jest": "^30.0.0",
"@types/node": "^24.7.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.1",
"@types/node": "^24.1.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^5.0.4",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.37.0",
"@vitejs/plugin-react": "^4.7.0",
"babel-plugin-react-compiler": "^19.1.0-rc.1",
"eslint": "^9.32.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-playwright": "^2.2.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.4.0",
"jest": "^30.2.0",
"stylelint": "^16.25.0",
"globals": "^16.3.0",
"jest": "^30.0.5",
"stylelint": "^16.23.0",
"stylelint-config-recommended": "^16.0.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-config-tailwindcss": "^1.0.0",
"tailwindcss": "^4.0.7",
"ts-jest": "^29.4.5",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.0",
"vite": "^7.1.9"
"ts-jest": "^29.4.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^7.0.6"
},
"jest": {
"preset": "ts-jest",

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -1,5 +1,3 @@
'use client';
import { Suspense } from 'react';
import { Outlet } from 'react-router';
import clsx from 'clsx';

View File

@ -1,5 +1,3 @@
'use client';
import { useNavigate, useRouteError } from 'react-router';
import { Button } from '@/components/control';

View File

@ -4,9 +4,6 @@ import React from 'react';
import { DialogType, useDialogsStore } from '@/stores/dialogs';
const DlgShowVideo = React.lazy(() =>
import('@/features/help/dialogs/dlg-show-video').then(module => ({ default: module.DlgShowVideo }))
);
const DlgChangeInputSchema = React.lazy(() =>
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
);
@ -49,8 +46,8 @@ const DlgDeleteOperation = React.lazy(() =>
}))
);
const DlgDeleteReference = React.lazy(() =>
import('@/features/oss/dialogs/dlg-delete-replica').then(module => ({
default: module.DlgDeleteReplica
import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
default: module.DlgDeleteReference
}))
);
const DlgEditEditors = React.lazy(() =>
@ -164,8 +161,6 @@ export const GlobalDialogs = () => {
return null;
}
switch (active) {
case DialogType.SHOW_VIDEO:
return <DlgShowVideo />;
case DialogType.CONSTITUENTA_TEMPLATE:
return <DlgCstTemplate />;
case DialogType.CREATE_CONSTITUENTA:

View File

@ -1,5 +1,3 @@
'use client';
import { useNavigation } from 'react-router';
import { useDebounce } from 'use-debounce';

View File

@ -1,3 +1,5 @@
'use client';
import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

View File

@ -1,5 +1,3 @@
'use client';
import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { usePreferencesStore } from '@/stores/preferences';

View File

@ -1,3 +1,5 @@
'use client';
import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants';

View File

@ -1,5 +1,3 @@
'use client';
import { useWindowSize } from '@/hooks/use-window-size';
import { usePreferencesStore } from '@/stores/preferences';

View File

@ -1,5 +1,3 @@
'use client';
import { useAuth } from '@/features/auth/backend/use-auth';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
@ -14,40 +12,34 @@ import { useConceptNavigation } from './navigation-context';
export function MenuAI() {
const router = useConceptNavigation();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
const menu = useDropdown();
const { user } = useAuth();
const showAIPrompt = useDialogsStore(state => state.showAIPrompt);
function navigateTemplates(event: React.MouseEvent<Element>) {
hideMenu();
menu.hide();
router.push({ path: urls.prompt_templates, newTab: event.ctrlKey || event.metaKey });
}
function handleCreatePrompt(event: React.MouseEvent<Element>) {
event.preventDefault();
event.stopPropagation();
hideMenu();
menu.hide();
showAIPrompt();
}
return (
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
<NavigationButton
title='ИИ помощник' //
hideTitle={isMenuOpen}
aria-expanded={isMenuOpen}
hideTitle={menu.isOpen}
aria-expanded={menu.isOpen}
aria-controls={globalIDs.ai_dropdown}
icon={<IconAssistant size='1.5rem' />}
onClick={toggleMenu}
onClick={menu.toggle}
/>
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={isMenuOpen}>
<Dropdown id={globalIDs.ai_dropdown} className='min-w-[12ch] max-w-48' stretchLeft isOpen={menu.isOpen}>
<DropdownButton
text='Запрос'
title='Создать запрос'

View File

@ -1,5 +1,3 @@
'use client';
import { Suspense } from 'react';
import { useDropdown } from '@/components/dropdown';
@ -13,23 +11,17 @@ import { UserDropdown } from './user-dropdown';
export function MenuUser() {
const router = useConceptNavigation();
const {
elementRef: menuRef,
isOpen: isMenuOpen,
toggle: toggleMenu,
handleBlur: handleMenuBlur,
hide: hideMenu
} = useDropdown();
const menu = useDropdown();
return (
<div ref={menuRef} onBlur={handleMenuBlur} className='flex items-center justify-start relative h-full'>
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center justify-start relative h-full'>
<Suspense fallback={<Loader circular scale={1.5} />}>
<UserButton
onLogin={() => router.push({ path: urls.login, force: true })}
onClickUser={toggleMenu}
isOpen={isMenuOpen}
onClickUser={menu.toggle}
isOpen={menu.isOpen}
/>
</Suspense>
<UserDropdown isOpen={isMenuOpen} hideDropdown={() => hideMenu()} />
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
</div>
);
}

View File

@ -1,5 +1,3 @@
'use client';
import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';

View File

@ -1,5 +1,3 @@
'use client';
import { useAuthSuspense } from '@/features/auth';
import { IconLogin, IconUser2 } from '@/components/icons';

View File

@ -1,5 +1,3 @@
'use client';
import { useAuthSuspense } from '@/features/auth';
import { useLogout } from '@/features/auth/backend/use-logout';

View File

@ -33,7 +33,7 @@ export function ExportDropdown<T extends object = object>({
filename = 'export',
className
}: ExportDropdownProps<T>) {
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
const { ref, isOpen, toggle, handleBlur, hide } = useDropdown();
function handleExport(format: 'csv' | 'json') {
if (!data || data.length === 0) {

View File

@ -1,4 +1,5 @@
'use no memo';
'use client';
import { type Table } from '@tanstack/react-table';

View File

@ -27,7 +27,7 @@ export function SelectPagination<TData>({ id, table, paginationOptions, onChange
);
return (
<Select onValueChange={handlePaginationOptionsChange} value={String(table.getState().pagination.pageSize)}>
<Select onValueChange={handlePaginationOptionsChange} defaultValue={String(table.getState().pagination.pageSize)}>
<SelectTrigger
id={id}
aria-label='Выбор количества строчек на странице'

View File

@ -1,4 +1,5 @@
'use no memo';
'use client';
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
import clsx from 'clsx';

View File

@ -1,4 +1,3 @@
'use client';
'use no memo';
import { useCallback } from 'react';

View File

@ -1,5 +1,4 @@
'use client';
'use no memo';
import { useState } from 'react';
import {

View File

@ -45,6 +45,7 @@ export function Dropdown({
margin,
className
)}
aria-hidden={!isOpen}
inert={!isOpen}
{...restProps}
>

View File

@ -4,26 +4,17 @@ import { useRef, useState } from 'react';
export function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const elementRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLDivElement>(null);
function handleBlur(event: React.FocusEvent<HTMLDivElement>) {
const nextTarget = event.relatedTarget as Node | null;
if (nextTarget && elementRef.current?.contains(nextTarget)) {
return;
}
// Keep open when focus moves into a popover (e.g., ComboBox menu rendered via portal)
if (
nextTarget instanceof Element &&
(nextTarget.closest("[data-slot='popover-content']") || nextTarget.closest("[data-slot='popover-trigger']"))
) {
if (ref.current?.contains(event.relatedTarget as Node)) {
return;
}
setIsOpen(false);
}
return {
elementRef,
ref,
isOpen,
setIsOpen,
handleBlur,

View File

@ -12,8 +12,8 @@ export { BiX as IconRemove } from 'react-icons/bi';
export { BiTrash as IconDestroy } from 'react-icons/bi';
export { BiReset as IconReset } from 'react-icons/bi';
export { TbArrowsDiagonal2 as IconResize } from 'react-icons/tb';
export { FiEdit as IconEdit } from 'react-icons/fi';
export { AiOutlineEdit as IconEdit2 } from 'react-icons/ai';
export { LiaEdit as IconEdit } from 'react-icons/lia';
export { FiEdit as IconEdit2 } from 'react-icons/fi';
export { BiSearchAlt2 as IconSearch } from 'react-icons/bi';
export { BiDownload as IconDownload } from 'react-icons/bi';
export { BiUpload as IconUpload } from 'react-icons/bi';
@ -39,7 +39,6 @@ export { RiMenuFoldFill as IconMenuFold } from 'react-icons/ri';
export { RiMenuUnfoldFill as IconMenuUnfold } from 'react-icons/ri';
export { LuMoon as IconDarkTheme } from 'react-icons/lu';
export { LuSun as IconLightTheme } from 'react-icons/lu';
export { IoVideocamOutline as IconVideo } from 'react-icons/io5';
export { LuFolderTree as IconFolderTree } from 'react-icons/lu';
export { LuFolder as IconFolder } from 'react-icons/lu';
export { LuFolderSearch as IconFolderSearch } from 'react-icons/lu';
@ -88,7 +87,6 @@ export { MdOutlineSelectAll as IconConceptBlock } from 'react-icons/md';
export { TbHexagon as IconRSForm } from 'react-icons/tb';
export { TbAssembly as IconRSFormOwned } from 'react-icons/tb';
export { TbBallFootball as IconRSFormImported } from 'react-icons/tb';
export { TbHexagonLetterN as IconCstNominal } from 'react-icons/tb';
export { TbHexagonLetterX as IconCstBaseSet } from 'react-icons/tb';
export { TbHexagonLetterC as IconCstConstSet } from 'react-icons/tb';
export { TbHexagonLetterS as IconCstStructured } from 'react-icons/tb';
@ -127,7 +125,7 @@ export { RiOpenSourceLine as IconPublic } from 'react-icons/ri';
export { RiShieldLine as IconProtected } from 'react-icons/ri';
export { RiShieldKeyholeLine as IconPrivate } from 'react-icons/ri';
export { BiBug as IconStatusError } from 'react-icons/bi';
export { LuThumbsUp as IconStatusOK } from 'react-icons/lu';
export { BiCheckCircle as IconStatusOK } from 'react-icons/bi';
export { BiHelpCircle as IconStatusUnknown } from 'react-icons/bi';
export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
@ -162,6 +160,7 @@ export { BiGitBranch as IconGraphInputs } from 'react-icons/bi';
export { TbEarScan as IconGraphInverse } from 'react-icons/tb';
export { BiGitMerge as IconGraphOutputs } from 'react-icons/bi';
export { LuAtom as IconGraphCore } from 'react-icons/lu';
export { LuRotate3D as IconRotate3D } from 'react-icons/lu';
export { MdOutlineFitScreen as IconFitImage } from 'react-icons/md';
export { RiFocus3Line as IconFocus } from 'react-icons/ri';
export { LuSparkles as IconClustering } from 'react-icons/lu';

View File

@ -1,7 +1,5 @@
'use client';
import assert from 'assert';
import { useRef, useState } from 'react';
import { ChevronDownIcon } from 'lucide-react';
@ -11,32 +9,20 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { cn } from '../utils';
interface ComboMultiPropsBase<Option> extends Styling {
interface ComboMultiProps<Option> extends Styling {
id?: string;
items?: Option[];
value: Option[];
onChange: (newValue: Option[]) => void;
idFunc: (item: Option) => string;
labelValueFunc: (item: Option) => string;
labelOptionFunc: (item: Option) => string;
disabled?: boolean;
placeholder?: string;
noSearch?: boolean;
}
interface ComboMultiPropsFull<Option> extends ComboMultiPropsBase<Option> {
onChange: (newValue: Option[]) => void;
}
interface ComboMultiPropsSplit<Option> extends ComboMultiPropsBase<Option> {
onClear: () => void;
onAdd: (item: Option) => void;
onRemove: (item: Option) => void;
}
type ComboMultiProps<Option> = ComboMultiPropsFull<Option> | ComboMultiPropsSplit<Option>;
/**
* Displays a combo-box component with multiple selection.
*/
@ -44,15 +30,14 @@ export function ComboMulti<Option>({
id,
items,
value,
onChange,
labelValueFunc,
labelOptionFunc,
idFunc,
placeholder,
className,
style,
disabled,
noSearch,
...restProps
noSearch
}: ComboMultiProps<Option>) {
const [open, setOpen] = useState(false);
const [popoverWidth, setPopoverWidth] = useState<number | undefined>(undefined);
@ -69,34 +54,19 @@ export function ComboMulti<Option>({
if (value.includes(newValue)) {
handleRemoveValue(newValue);
} else {
if ('onAdd' in restProps && typeof restProps.onAdd === 'function') {
restProps.onAdd(newValue);
} else {
assert('onChange' in restProps);
restProps.onChange([...value, newValue]);
}
onChange([...value, newValue]);
setOpen(false);
}
}
function handleRemoveValue(delValue: Option) {
if ('onRemove' in restProps && typeof restProps.onRemove === 'function') {
restProps.onRemove(delValue);
} else {
assert('onChange' in restProps);
restProps.onChange(value.filter(v => v !== delValue));
}
onChange(value.filter(v => v !== delValue));
setOpen(false);
}
function handleClear(event: React.MouseEvent<SVGElement>) {
event.stopPropagation();
if ('onClear' in restProps && typeof restProps.onClear === 'function') {
restProps.onClear();
} else {
assert('onChange' in restProps);
restProps.onChange([]);
}
onChange([]);
setOpen(false);
}
@ -111,7 +81,7 @@ export function ComboMulti<Option>({
className={cn(
'relative h-9',
'flex gap-2 px-3 py-2 items-center justify-between',
'bg-input disabled:bg-transparent',
'bg-input disabled:opacity-50',
'cursor-pointer disabled:cursor-auto',
'whitespace-nowrap',
'focus-outline border',
@ -121,39 +91,32 @@ export function ComboMulti<Option>({
className
)}
style={style}
disabled={disabled}
>
<div className='flex flex-wrap gap-2 items-center'>
<div className='flex flex-wrap gap-1 items-center'>
{value.length === 0 ? <div className='text-muted-foreground'>{placeholder}</div> : null}
{value.map(item => (
<div key={idFunc(item)} className='flex px-1 items-center border rounded-lg bg-accent text-sm'>
{labelValueFunc(item)}
{!disabled ? (
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove cc-hover-pulse'
onClick={
disabled
? undefined
: event => {
event.stopPropagation();
handleRemoveValue(item);
}
}
/>
) : null}
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove cc-hover-pulse'
onClick={event => {
event.stopPropagation();
handleRemoveValue(item);
}}
/>
</div>
))}
</div>
<ChevronDownIcon className={cn('text-muted-foreground', !!value && 'opacity-0')} />
{!!value && !disabled ? (
{!!value ? (
<IconRemove
tabIndex={-1}
size='1rem'
className='cc-remove absolute pointer-events-auto right-3 cc-hover-pulse hover:text-primary'
onClick={value.length === 0 ? undefined : handleClear}
onClick={handleClear}
/>
) : null}
</button>
@ -164,19 +127,16 @@ export function ComboMulti<Option>({
<CommandList>
<CommandEmpty>Список пуст</CommandEmpty>
<CommandGroup>
{items
?.filter(item => !value.includes(item))
.map(item => (
<CommandItem
key={idFunc(item)}
value={labelOptionFunc(item)}
onSelect={() => handleAddValue(item)}
disabled={disabled}
className={cn(value === item && 'bg-selected text-selected-foreground')}
>
{labelOptionFunc(item)}
</CommandItem>
))}
{items?.map(item => (
<CommandItem
key={idFunc(item)}
value={labelOptionFunc(item)}
onSelect={() => handleAddValue(item)}
className={cn(value === item && 'bg-selected text-selected-foreground')}
>
{labelOptionFunc(item)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>

View File

@ -1,5 +1,3 @@
'use client';
import { useEffect, useState } from 'react';
import clsx from 'clsx';

View File

@ -1,42 +0,0 @@
interface EmbedVKVideoProps {
/** Video ID to embed. */
videoID: string;
/** Display height in pixels. */
pxHeight: number;
/** Display width in pixels. */
pxWidth?: number;
}
/**
* Embeds a YouTube video into the page using the given video ID and dimensions.
*/
export function EmbedVKVideo({ videoID, pxHeight, pxWidth }: EmbedVKVideoProps) {
if (!pxWidth) {
pxWidth = (pxHeight * 16) / 9;
}
return (
<div
className='relative h-0 mt-1'
style={{
paddingBottom: `${pxHeight}px`,
paddingLeft: `${pxWidth}px`
}}
>
<iframe
allowFullScreen
title='Встроенное видео ВКонтакте'
allow='autoplay; encrypted-media; fullscreen; picture-in-picture; screen-wake-lock;'
className='absolute top-0 left-0 border'
style={{
minHeight: `${pxHeight}px`,
minWidth: `${pxWidth}px`
}}
width={`${pxWidth}px`}
height={`${pxHeight}px`}
src={`https://vk.com/video_ext.php?${videoID}&hd=1`}
/>
</div>
);
}

View File

@ -1,5 +1,3 @@
'use client';
import { Suspense, useState } from 'react';
import { HelpTopic } from '@/features/help';
@ -20,7 +18,7 @@ export function DlgAIPromptDialog() {
return (
<ModalView
header='Генератор запросом LLM'
className='w-100 sm:w-160 px-6 flex flex-col h-110'
className='w-100 sm:w-160 px-6 flex flex-col h-120'
helpTopic={HelpTopic.ASSISTANT}
>
<ComboBox

View File

@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import { urls, useConceptNavigation } from '@/app';
import { MiniButton } from '@/components/control';
import { IconClone, IconEdit } from '@/components/icons';
import { IconClone, IconEdit2 } from '@/components/icons';
import { useDialogsStore } from '@/stores/dialogs';
import { infoMsg } from '@/utils/labels';
@ -36,7 +36,7 @@ export function MenuAIPrompt({ promptID, generatedPrompt }: MenuAIPromptProps) {
title='Редактировать шаблон'
noHover
noPadding
icon={<IconEdit size='1.25rem' />}
icon={<IconEdit2 size='1.25rem' />}
className='h-full pl-2 text-muted-foreground hover:text-primary cc-animate-color bg-transparent'
onClick={navigatePrompt}
/>

View File

@ -1,5 +1,3 @@
'use client';
import { TextArea } from '@/components/input';
import { PromptInput } from '../../components/prompt-input';

View File

@ -11,7 +11,7 @@ export function TabPromptResult({ prompt }: TabPromptResultProps) {
value={prompt}
placeholder='Текст шаблона пуст'
disabled
className='w-full h-88'
className='w-full h-100'
/>
);
}

View File

@ -1,5 +1,3 @@
'use client';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
@ -41,7 +39,7 @@ export function DlgCreatePromptTemplate() {
mode: 'onChange'
});
const label = useWatch({ control, name: 'label' });
const canSubmit = !!label && !templates.find(template => template.label === label);
const isValid = !!label && !templates.find(template => template.label === label);
function onSubmit(data: ICreatePromptTemplateDTO) {
void createPromptTemplate(data).then(onCreate);
@ -51,7 +49,7 @@ export function DlgCreatePromptTemplate() {
<ModalForm
header='Создание шаблона'
submitText='Создать'
canSubmit={canSubmit}
canSubmit={isValid}
onSubmit={event => void handleSubmit(onSubmit)(event)}
submitInvalidTooltip='Введите уникальное название шаблона'
className='cc-column w-140 max-h-120 py-2 px-6'

View File

@ -4,7 +4,6 @@ import { labelCstTypification } from '@/features/rsform/labels';
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
import { TypificationGraph } from '@/features/rsform/models/typification-graph';
import { type Graph } from '@/models/graph';
import { PARAMETER } from '@/utils/constants';
import { mockPromptVariable } from '../labels';
@ -39,24 +38,30 @@ export function generateSample(target: string): string {
/** Generates a prompt for a schema variable. */
export function varSchema(schema: IRSForm): string {
let result = stringifySchemaIntro(schema);
result += '\n\nКонституенты:';
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}"`;
});
result += `\n${stringifyCrucial(schema.items.filter(cst => cst.crucial))}`;
result += '\n\nСвязи "атрибутирован":';
const attributionGraph = stringifyGraph(schema.attribution_graph, schema);
result += attributionGraph ? attributionGraph : ' отсутствуют';
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 = stringifySchemaIntro(schema);
result += '\n\nТермины:';
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;
@ -72,62 +77,48 @@ export function varSchemaThesaurus(schema: IRSForm): string {
/** Generates a prompt for a schema graph variable. */
export function varSchemaGraph(schema: IRSForm): string {
let result = stringifySchemaIntro(schema);
result += '\n\nУзлы графа\n';
result += JSON.stringify(
schema.items.map(cst => ({
alias: cst.alias,
term: cst.term_resolved,
definition: cst.definition_resolved,
convention: cst.convention,
crucial: cst.crucial
})),
null,
PARAMETER.indentJSON
);
result += '\n\nСвязи "входит в определение"';
const definitionGraph = stringifyGraph(schema.graph, schema);
result += definitionGraph ? definitionGraph : ' отсутствуют';
result += '\n\nСвязи "атрибутирован"';
const attributionGraph = stringifyGraph(schema.attribution_graph, schema);
result += attributionGraph ? attributionGraph : ' отсутствуют';
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 => {
if (item.parse) graph.addConstituenta(item.alias, item.parse.typification, item.parse.args);
});
schema.items.forEach(item => graph.addConstituenta(item.alias, item.parse.typification, item.parse.args));
let result = stringifySchemaIntro(schema);
result += '\n\nСтупени\n';
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 = stringifyOSSIntro(oss);
result += `\n\nБлоки: ${oss.blocks.length}\n`;
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\nБлок ${block.id}: ${block.title}`;
result += `\nОписание: ${block.description}`;
result += `\nПредок: "${block.parent ?? 'отсутствует'}"`;
result += `\nБлок ${block.id}: ${block.title}\n`;
result += `Описание: ${block.description}\n`;
result += `Предок: "${block.parent}"\n`;
});
result += `\n\nОперации: ${oss.operations.length}`;
result += `Операции: ${oss.operations.length}\n`;
oss.operations.forEach(operation => {
result += `\n\nОперация ${operation.id}: ${operation.alias}`;
result += `\nНазвание: ${operation.title}`;
result += `\nОписание: ${operation.description}`;
result += `\nБлок: ${operation.parent ?? 'отсутствует'}`;
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
result += `Название: ${operation.title}\n`;
result += `Описание: ${operation.description}\n`;
result += `Блок: ${operation.parent}`;
});
return result;
}
@ -136,19 +127,19 @@ export function varOSS(oss: IOperationSchema): string {
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}`;
result += `\nОписание: "${target.description}"`;
result += '\n\nСодержание';
result += `\nБлоки: ${blocks.length}`;
let result = `Название блока: ${target.title}\n`;
result += `Описание: "${target.description}"\n`;
result += '\nСодержание\n';
result += `Блоки: ${blocks.length}\n`;
blocks.forEach(block => {
result += `\n\nБлок ${block.id}: ${block.title}`;
result += `\nОписание: "${block.description}"`;
result += `\nБлок ${block.id}: ${block.title}\n`;
result += `Описание: "${block.description}"\n`;
});
result += `\n\nОперации: ${operations.length}`;
result += `Операции: ${operations.length}\n`;
operations.forEach(operation => {
result += `\n\nОперация ${operation.id}: ${operation.alias}`;
result += `\nНазвание: "${operation.title}"`;
result += `\nОписание: "${operation.description}"`;
result += `\nОперация ${operation.id}: ${operation.alias}\n`;
result += `Название: "${operation.title}"\n`;
result += `Описание: "${operation.description}"`;
});
return result;
}
@ -160,49 +151,9 @@ export function varConstituenta(cst: IConstituenta): string {
/** Generates a prompt for a constituenta syntax tree variable. */
export function varSyntaxTree(cst: IConstituenta): string {
let result = `Конституента: ${cst.alias}`;
result += `\ормальное выражение: ${cst.definition_formal}`;
result += `\ерево синтаксического разбора:\n`;
result += cst.parse ? JSON.stringify(cst.parse.syntaxTree, null, PARAMETER.indentJSON) : 'не определено';
return result;
}
// ==== Internal functions ====
function stringifyGraph(graph: Graph<number>, schema: IRSForm): string {
let result = '';
graph.nodes.forEach(node => {
if (node.outputs.length > 0) {
result += `\n${schema.items.find(cst => cst.id === node.id)!.alias} -> ${node.outputs
.map(id => schema.items.find(cst => cst.id === id)!.alias)
.join(', ')}`;
}
});
return result;
}
function stringifySchemaIntro(schema: IRSForm): string {
let result = `Концептуальная схема: ${schema.title}`;
result += `\nКраткое название: ${schema.alias}`;
if (schema.description) {
result += `\nОписание: "${schema.description}"`;
}
return result;
}
function stringifyOSSIntro(schema: IOperationSchema): string {
let result = `Операционная схема: ${schema.title}`;
result += `\nКраткое название: ${schema.alias}`;
if (schema.description) {
result += `\nОписание: "${schema.description}"`;
}
return result;
}
function stringifyCrucial(cstList: IConstituenta[]): string {
let result = 'Ключевые конституенты: ';
if (cstList.length === 0) {
return result + 'отсутствуют';
}
result += cstList.map(cst => cst.alias).join(', ');
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,5 +1,3 @@
'use client';
import { urls, useConceptNavigation } from '@/app';
import { MiniButton } from '@/components/control';

View File

@ -1,5 +1,3 @@
'use client';
import { ErrorBoundary } from 'react-error-boundary';
import { isAxiosError } from 'axios';
import { z } from 'zod';

View File

@ -1,7 +1,7 @@
'use no memo'; // TODO: remove when react hook forms are compliant with react compiler
'use client';
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import clsx from 'clsx';
@ -63,25 +63,24 @@ export function FormPromptTemplate({ promptTemplate, className, isMutable, toggl
const prevReset = useRef(toggleReset);
const prevTemplate = useRef(promptTemplate);
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
prevTemplate.current = promptTemplate;
prevReset.current = toggleReset;
reset({
owner: promptTemplate.owner,
label: promptTemplate.label,
description: promptTemplate.description,
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
});
setSampleResult(null);
}
useEffect(() => {
if (prevTemplate.current !== promptTemplate || prevReset.current !== toggleReset) {
prevTemplate.current = promptTemplate;
prevReset.current = toggleReset;
reset({
owner: promptTemplate.owner,
label: promptTemplate.label,
description: promptTemplate.description,
text: promptTemplate.text,
is_shared: promptTemplate.is_shared
});
return () => setSampleResult(null);
}
}, [promptTemplate, toggleReset, reset, setSampleResult]);
useEffect(() => {
const prevDirty = useRef(isDirty);
if (prevDirty.current !== isDirty) {
prevDirty.current = isDirty;
setIsModified(isDirty);
}, [isDirty, setIsModified]);
}
function onSubmit(data: IUpdatePromptTemplateDTO) {
return updatePromptTemplate({ id: promptTemplate.id, data }).then(() => {

View File

@ -1,5 +1,3 @@
'use client';
import { useState } from 'react';
import clsx from 'clsx';

View File

@ -1,5 +1,3 @@
'use client';
import { urls, useConceptNavigation } from '@/app';
import { useDeletePromptTemplate } from '@/features/ai/backend/use-delete-prompt-template';
import { useMutatingPrompts } from '@/features/ai/backend/use-mutating-prompts';

Some files were not shown because too many files have changed in this diff Show More