Compare commits
No commits in common. "f3d361f8b45e1d3a298943ebab2303f21d88cc3c" and "6b2268a76b096889a5c18beb5f3a457931d807e2" have entirely different histories.
f3d361f8b4
...
6b2268a76b
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
|
@ -185,7 +185,6 @@
|
||||||
"Акименков",
|
"Акименков",
|
||||||
"Астрина",
|
"Астрина",
|
||||||
"Атрибутирование",
|
"Атрибутирование",
|
||||||
"Атрибутирования",
|
|
||||||
"Атрибутирующая",
|
"Атрибутирующая",
|
||||||
"Атрибутирующие",
|
"Атрибутирующие",
|
||||||
"Ашихмин",
|
"Ашихмин",
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
<pre>
|
<pre>
|
||||||
- axios
|
- axios
|
||||||
- clsx
|
- clsx
|
||||||
- dompurify
|
|
||||||
- react-icons
|
- react-icons
|
||||||
- react-router
|
- react-router
|
||||||
- react-toastify
|
- react-toastify
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ ignored-modules=
|
||||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||||
# number of processors available to use, and will cap the count on Windows to
|
# number of processors available to use, and will cap the count on Windows to
|
||||||
# avoid hangs.
|
# avoid hangs.
|
||||||
jobs=0
|
jobs=1
|
||||||
|
|
||||||
# Control the amount of potential inferred values when inferring a single
|
# Control the amount of potential inferred values when inferring a single
|
||||||
# object. This can help the performance when dealing with large functions or
|
# object. This can help the performance when dealing with large functions or
|
||||||
|
|
@ -88,7 +88,7 @@ persistent=yes
|
||||||
|
|
||||||
# Minimum Python version to use for version dependent checks. Will default to
|
# Minimum Python version to use for version dependent checks. Will default to
|
||||||
# the version used to run pylint.
|
# the version used to run pylint.
|
||||||
py-version=3.10
|
py-version=3.9
|
||||||
|
|
||||||
# Discover python modules and packages in the file system subtree.
|
# Discover python modules and packages in the file system subtree.
|
||||||
recursive=no
|
recursive=no
|
||||||
|
|
@ -99,6 +99,10 @@ recursive=no
|
||||||
# source root.
|
# source root.
|
||||||
source-roots=
|
source-roots=
|
||||||
|
|
||||||
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
suggestion-mode=yes
|
||||||
|
|
||||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||||
# active Python interpreter and may run arbitrary code.
|
# active Python interpreter and may run arbitrary code.
|
||||||
unsafe-load-any-extension=no
|
unsafe-load-any-extension=no
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class LibraryItemCloneSerializer(StrictSerializer):
|
||||||
model = LibraryItem
|
model = LibraryItem
|
||||||
exclude = ['id', 'item_type', 'owner', 'read_only']
|
exclude = ['id', 'item_type', 'owner', 'read_only']
|
||||||
|
|
||||||
items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id'))
|
items = PKField(many=True, queryset=Constituenta.objects.all().only('pk', 'schema_id'))
|
||||||
item_data = ItemCloneData()
|
item_data = ItemCloneData()
|
||||||
|
|
||||||
def validate_items(self, value):
|
def validate_items(self, value):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
''' Testing API: Library. '''
|
''' Testing API: Library. '''
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from apps.library.models import (
|
from apps.library.models import (
|
||||||
|
|
@ -360,8 +358,6 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
||||||
self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw)
|
self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw)
|
||||||
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
|
||||||
self.assertEqual(response.data['attribution'][0]['attribute'], response.data['items'][0]['id'])
|
|
||||||
self.assertEqual(response.data['attribution'][0]['container'], response.data['items'][1]['id'])
|
|
||||||
|
|
||||||
data = {'item_data': {'title': 'Title1340'}, 'items': []}
|
data = {'item_data': {'title': 'Title1340'}, 'items': []}
|
||||||
response = self.executeCreated(data, item=self.owned.pk)
|
response = self.executeCreated(data, item=self.owned.pk)
|
||||||
|
|
@ -375,27 +371,3 @@ class TestLibraryViewset(EndpointTester):
|
||||||
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
|
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_raw'], x12.term_raw)
|
||||||
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/library/{item}/clone', method='post')
|
|
||||||
def test_clone_rsform_partial(self):
|
|
||||||
schema = RSForm(self.owned)
|
|
||||||
x1 = schema.insert_last(alias='X1')
|
|
||||||
x2 = schema.insert_last(alias='X2')
|
|
||||||
d1 = schema.insert_last(alias='D1')
|
|
||||||
|
|
||||||
Attribution.objects.create(container=x2, attribute=x1)
|
|
||||||
Attribution.objects.create(container=d1, attribute=x2)
|
|
||||||
|
|
||||||
# Only clone x2 and d1
|
|
||||||
data = {'item_data': {'title': 'Cloned'}, 'items': [x2.pk, d1.pk]}
|
|
||||||
response = self.executeCreated(data, item=self.owned.pk)
|
|
||||||
self.assertEqual(response.data['title'], data['item_data']['title'])
|
|
||||||
self.assertEqual(len(response.data['items']), 2)
|
|
||||||
|
|
||||||
aliases = set[Any](item['alias'] for item in response.data['items'])
|
|
||||||
self.assertIn(x2.alias, aliases)
|
|
||||||
self.assertIn(d1.alias, aliases)
|
|
||||||
self.assertEqual(len(response.data['attribution']), 1)
|
|
||||||
self.assertEqual(response.data['attribution'][0]['container'], response.data['items'][1]['id'])
|
|
||||||
self.assertEqual(response.data['attribution'][0]['attribute'], response.data['items'][0]['id'])
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade
|
||||||
from apps.rsform.models import RSFormCached
|
from apps.rsform.models import Attribution, RSFormCached
|
||||||
from apps.rsform.serializers import RSFormParseSerializer
|
from apps.rsform.serializers import RSFormParseSerializer
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared import permissions
|
from shared import permissions
|
||||||
|
|
@ -67,7 +67,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
|
|
||||||
def perform_destroy(self, instance: m.LibraryItem) -> None:
|
def perform_destroy(self, instance: m.LibraryItem) -> None:
|
||||||
if instance.item_type == m.LibraryItemType.RSFORM:
|
if instance.item_type == m.LibraryItemType.RSFORM:
|
||||||
PropagationFacade().before_delete_schema(instance.pk)
|
PropagationFacade.before_delete_schema(instance)
|
||||||
super().perform_destroy(instance)
|
super().perform_destroy(instance)
|
||||||
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
if instance.item_type == m.LibraryItemType.OPERATION_SCHEMA:
|
||||||
schemas = list(OperationSchema.owned_schemasQ(instance))
|
schemas = list(OperationSchema.owned_schemasQ(instance))
|
||||||
|
|
@ -172,7 +172,22 @@ class LibraryViewSet(viewsets.ModelViewSet):
|
||||||
clone.location = data.get('location', m.LocationHead.USER)
|
clone.location = data.get('location', m.LocationHead.USER)
|
||||||
clone.save()
|
clone.save()
|
||||||
|
|
||||||
RSFormCached(clone.pk).insert_from(item.pk, request.data['items'] if 'items' in request.data else None)
|
cst_map: dict[int, int] = {}
|
||||||
|
cst_list: list[int] = []
|
||||||
|
need_filter = 'items' in request.data and request.data['items']
|
||||||
|
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(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
''' Models: Synthesis Inheritance. '''
|
''' Models: Synthesis Inheritance. '''
|
||||||
from django.db.models import CASCADE, ForeignKey, Model
|
from django.db.models import CASCADE, ForeignKey, Model
|
||||||
|
|
||||||
from .Substitution import Substitution
|
|
||||||
|
|
||||||
|
|
||||||
class Inheritance(Model):
|
class Inheritance(Model):
|
||||||
''' Inheritance links parent and child constituents in synthesis operation.'''
|
''' Inheritance links parent and child constituents in synthesis operation.'''
|
||||||
|
|
@ -34,32 +32,3 @@ class Inheritance(Model):
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f'{self.parent} -> {self.child}'
|
return f'{self.parent} -> {self.child}'
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def check_share_origin(cst1: int, cst2: int) -> bool:
|
|
||||||
''' Check if two constituents share origin. '''
|
|
||||||
inheritance1 = Inheritance.objects.filter(child_id=cst1).first()
|
|
||||||
if not inheritance1:
|
|
||||||
return False
|
|
||||||
inheritance2 = Inheritance.objects.filter(child_id=cst2).first()
|
|
||||||
if not inheritance2:
|
|
||||||
return False
|
|
||||||
|
|
||||||
parent1 = inheritance1.parent
|
|
||||||
parent2 = inheritance2.parent
|
|
||||||
|
|
||||||
origins1 = list(
|
|
||||||
Substitution.objects.filter(
|
|
||||||
substitution=parent1).values_list(
|
|
||||||
'original__schema_id',
|
|
||||||
flat=True))
|
|
||||||
origins1.append(parent1.schema_id)
|
|
||||||
|
|
||||||
origins2 = list(
|
|
||||||
Substitution.objects.filter(
|
|
||||||
substitution=parent2).values_list(
|
|
||||||
'original__schema_id',
|
|
||||||
flat=True))
|
|
||||||
origins2.append(parent2.schema_id)
|
|
||||||
|
|
||||||
return any(x in origins1 for x in origins2)
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class Layout(Model):
|
||||||
return f'Схема расположения {self.oss.alias}'
|
return f'Схема расположения {self.oss.alias}'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_data(itemID: int, data: list) -> None:
|
def update_data(itemID: int, data: dict) -> None:
|
||||||
''' Update layout data. '''
|
''' Update layout data. '''
|
||||||
layout = Layout.objects.get(oss_id=itemID)
|
layout = Layout.objects.get(oss_id=itemID)
|
||||||
layout.data = data
|
layout.data = data
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from .Substitution import Substitution
|
||||||
class OperationSchema:
|
class OperationSchema:
|
||||||
''' Operations schema API wrapper. No caching, propagation and minimal side effects. '''
|
''' Operations schema API wrapper. No caching, propagation and minimal side effects. '''
|
||||||
|
|
||||||
def __init__(self, model: LibraryItem) -> None:
|
def __init__(self, model: LibraryItem):
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -43,18 +43,19 @@ class OperationSchema:
|
||||||
return Layout.objects.get(oss_id=itemID)
|
return Layout.objects.get(oss_id=itemID)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_input(oss_id: int, operation: Operation) -> LibraryItem:
|
def create_input(oss: LibraryItem, operation: Operation) -> RSFormCached:
|
||||||
''' Create input RSForm for given Operation. '''
|
''' Create input RSForm for given Operation. '''
|
||||||
oss = LibraryItem.objects.get(pk=oss_id)
|
schema = RSFormCached.create(
|
||||||
schema = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, owner=oss.owner,
|
owner=oss.owner,
|
||||||
alias=operation.alias,
|
alias=operation.alias,
|
||||||
title=operation.title,
|
title=operation.title,
|
||||||
description=operation.description,
|
description=operation.description,
|
||||||
visible=False,
|
visible=False,
|
||||||
access_policy=oss.access_policy,
|
access_policy=oss.access_policy,
|
||||||
location=oss.location)
|
location=oss.location
|
||||||
Editor.set(schema.pk, oss.getQ_editors().values_list('pk', flat=True))
|
)
|
||||||
operation.setQ_result(schema)
|
Editor.set(schema.model.pk, oss.getQ_editors().values_list('pk', flat=True))
|
||||||
|
operation.setQ_result(schema.model)
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def refresh_from_db(self) -> None:
|
def refresh_from_db(self) -> None:
|
||||||
|
|
@ -131,15 +132,16 @@ class OperationSchema:
|
||||||
if not schemas:
|
if not schemas:
|
||||||
return
|
return
|
||||||
substitutions = operation.getQ_substitutions()
|
substitutions = operation.getQ_substitutions()
|
||||||
receiver = RSFormCached(OperationSchema.create_input(self.model.pk, operation).pk)
|
receiver = OperationSchema.create_input(self.model, operation)
|
||||||
|
|
||||||
parents: dict = {}
|
parents: dict = {}
|
||||||
children: dict = {}
|
children: dict = {}
|
||||||
for operand in schemas:
|
for operand in schemas:
|
||||||
new_items = receiver.insert_from(operand)
|
items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
|
||||||
for (old_cst, new_cst) in new_items:
|
new_items = receiver.insert_copy(items)
|
||||||
parents[new_cst.pk] = old_cst
|
for (i, cst) in enumerate(new_items):
|
||||||
children[old_cst.pk] = new_cst
|
parents[cst.pk] = items[i]
|
||||||
|
children[items[i].pk] = cst
|
||||||
|
|
||||||
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
|
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
|
||||||
for sub in substitutions:
|
for sub in substitutions:
|
||||||
|
|
@ -148,7 +150,7 @@ class OperationSchema:
|
||||||
translated_substitutions.append((original, replacement))
|
translated_substitutions.append((original, replacement))
|
||||||
receiver.substitute(translated_substitutions)
|
receiver.substitute(translated_substitutions)
|
||||||
|
|
||||||
for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'):
|
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
|
||||||
parent = parents.get(cst.pk)
|
parent = parents.get(cst.pk)
|
||||||
assert parent is not None
|
assert parent is not None
|
||||||
Inheritance.objects.create(
|
Inheritance.objects.create(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ from .Inheritance import Inheritance
|
||||||
from .Operation import Operation
|
from .Operation import Operation
|
||||||
from .OperationSchema import OperationSchema
|
from .OperationSchema import OperationSchema
|
||||||
from .OssCache import OssCache
|
from .OssCache import OssCache
|
||||||
from .PropagationContext import PropagationContext
|
|
||||||
from .PropagationEngine import PropagationEngine
|
from .PropagationEngine import PropagationEngine
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
|
from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extract_data_references
|
||||||
|
|
@ -20,11 +19,10 @@ from .utils import CstMapping, CstSubstitution, create_dependant_mapping, extrac
|
||||||
class OperationSchemaCached:
|
class OperationSchemaCached:
|
||||||
''' Operations schema API with caching. '''
|
''' Operations schema API with caching. '''
|
||||||
|
|
||||||
def __init__(self, item_id: int, context: PropagationContext) -> None:
|
def __init__(self, model: LibraryItem):
|
||||||
self.pk = item_id
|
self.model = model
|
||||||
self.context = context
|
self.cache = OssCache(model.pk)
|
||||||
self.cache = OssCache(item_id, context)
|
self.engine = PropagationEngine(self.cache)
|
||||||
self.engine = PropagationEngine(self.cache, context)
|
|
||||||
|
|
||||||
def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
def delete_replica(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
|
||||||
''' Delete Replica Operation. '''
|
''' Delete Replica Operation. '''
|
||||||
|
|
@ -55,7 +53,7 @@ class OperationSchemaCached:
|
||||||
inheritance_to_delete: list[Inheritance] = []
|
inheritance_to_delete: list[Inheritance] = []
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
child_schema = self.cache.get_result(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
if child_schema is None:
|
if child_schema is None:
|
||||||
continue
|
continue
|
||||||
self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
|
self.engine.undo_substitutions_cst(ids, child_operation, child_schema)
|
||||||
|
|
@ -72,15 +70,15 @@ class OperationSchemaCached:
|
||||||
''' Set input schema for operation. '''
|
''' Set input schema for operation. '''
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
has_children = bool(self.cache.extend_graph.outputs[target])
|
has_children = bool(self.cache.extend_graph.outputs[target])
|
||||||
old_schema = self.cache.get_result(operation)
|
old_schema = self.cache.get_schema(operation)
|
||||||
if schema is None and old_schema is None or \
|
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.pk):
|
(schema is not None and old_schema is not None and schema.pk == old_schema.model.pk):
|
||||||
return
|
return
|
||||||
|
|
||||||
if old_schema is not None:
|
if old_schema is not None:
|
||||||
if has_children:
|
if has_children:
|
||||||
self.before_delete_cst(old_schema.pk, [cst.pk for cst in old_schema.cache.constituents])
|
self.before_delete_cst(old_schema.model.pk, [cst.pk for cst in old_schema.cache.constituents])
|
||||||
self.context.invalidate(old_schema.pk)
|
self.cache.remove_schema(old_schema)
|
||||||
|
|
||||||
operation.setQ_result(schema)
|
operation.setQ_result(schema)
|
||||||
if schema is not None:
|
if schema is not None:
|
||||||
|
|
@ -90,8 +88,8 @@ class OperationSchemaCached:
|
||||||
operation.save(update_fields=['alias', 'title', 'description'])
|
operation.save(update_fields=['alias', 'title', 'description'])
|
||||||
|
|
||||||
if schema is not None and has_children:
|
if schema is not None and has_children:
|
||||||
cst_list = list(Constituenta.objects.filter(schema_id=schema.pk).order_by('order'))
|
rsform = RSFormCached(schema)
|
||||||
self.after_create_cst(schema.pk, cst_list)
|
self.after_create_cst(rsform, list(rsform.constituentsQ().order_by('order')))
|
||||||
|
|
||||||
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
def set_arguments(self, target: int, arguments: list[Operation]) -> None:
|
||||||
''' Set arguments of target Operation. '''
|
''' Set arguments of target Operation. '''
|
||||||
|
|
@ -128,7 +126,7 @@ class OperationSchemaCached:
|
||||||
''' Clear all arguments for target Operation. '''
|
''' Clear all arguments for target Operation. '''
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
schema = self.cache.get_result(operation)
|
schema = self.cache.get_schema(operation)
|
||||||
processed: list[dict] = []
|
processed: list[dict] = []
|
||||||
deleted: list[Substitution] = []
|
deleted: list[Substitution] = []
|
||||||
for current in operation.getQ_substitutions():
|
for current in operation.getQ_substitutions():
|
||||||
|
|
@ -174,16 +172,17 @@ class OperationSchemaCached:
|
||||||
if not schemas:
|
if not schemas:
|
||||||
return False
|
return False
|
||||||
substitutions = operation.getQ_substitutions()
|
substitutions = operation.getQ_substitutions()
|
||||||
new_schema = OperationSchema.create_input(self.pk, self.cache.operation_by_id[operation.pk])
|
receiver = OperationSchema.create_input(self.model, self.cache.operation_by_id[operation.pk])
|
||||||
receiver = self.context.get_schema(new_schema.pk)
|
self.cache.insert_schema(receiver)
|
||||||
|
|
||||||
parents: dict = {}
|
parents: dict = {}
|
||||||
children: dict = {}
|
children: dict = {}
|
||||||
for operand in schemas:
|
for operand in schemas:
|
||||||
new_items = receiver.insert_from(operand)
|
items = list(Constituenta.objects.filter(schema_id=operand).order_by('order'))
|
||||||
for (old_cst, new_cst) in new_items:
|
new_items = receiver.insert_copy(items)
|
||||||
parents[new_cst.pk] = old_cst
|
for (i, cst) in enumerate(new_items):
|
||||||
children[old_cst.pk] = new_cst
|
parents[cst.pk] = items[i]
|
||||||
|
children[items[i].pk] = cst
|
||||||
|
|
||||||
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
|
translated_substitutions: list[tuple[Constituenta, Constituenta]] = []
|
||||||
for sub in substitutions:
|
for sub in substitutions:
|
||||||
|
|
@ -192,7 +191,7 @@ class OperationSchemaCached:
|
||||||
translated_substitutions.append((original, replacement))
|
translated_substitutions.append((original, replacement))
|
||||||
receiver.substitute(translated_substitutions)
|
receiver.substitute(translated_substitutions)
|
||||||
|
|
||||||
for cst in Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'):
|
for cst in Constituenta.objects.filter(schema=receiver.model).order_by('order'):
|
||||||
parent = parents.get(cst.pk)
|
parent = parents.get(cst.pk)
|
||||||
assert parent is not None
|
assert parent is not None
|
||||||
Inheritance.objects.create(
|
Inheritance.objects.create(
|
||||||
|
|
@ -206,31 +205,31 @@ class OperationSchemaCached:
|
||||||
receiver.resolve_all_text()
|
receiver.resolve_all_text()
|
||||||
|
|
||||||
if self.cache.extend_graph.outputs[operation.pk]:
|
if self.cache.extend_graph.outputs[operation.pk]:
|
||||||
receiver_items = list(Constituenta.objects.filter(schema_id=receiver.pk).order_by('order'))
|
receiver_items = list(Constituenta.objects.filter(schema=receiver.model).order_by('order'))
|
||||||
self.after_create_cst(receiver.pk, receiver_items)
|
self.after_create_cst(receiver, receiver_items)
|
||||||
|
receiver.model.save(update_fields=['time_update'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def relocate_down(self, destinationID: int, items: list[int]):
|
def relocate_down(self, source: RSFormCached, destination: RSFormCached, items: list[int]):
|
||||||
''' Move list of Constituents to destination Schema inheritor. '''
|
''' Move list of Constituents to destination Schema inheritor. '''
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
|
self.cache.insert_schema(source)
|
||||||
operation = self.cache.get_operation(destinationID)
|
self.cache.insert_schema(destination)
|
||||||
destination = self.context.get_schema(destinationID)
|
operation = self.cache.get_operation(destination.model.pk)
|
||||||
self.engine.undo_substitutions_cst(items, operation, destination)
|
self.engine.undo_substitutions_cst(items, operation, destination)
|
||||||
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items]
|
||||||
for item in inheritance_to_delete:
|
for item in inheritance_to_delete:
|
||||||
self.cache.remove_inheritance(item)
|
self.cache.remove_inheritance(item)
|
||||||
Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete()
|
Inheritance.objects.filter(operation_id=operation.pk, parent_id__in=items).delete()
|
||||||
|
|
||||||
def relocate_up(self, sourceID: int, destinationID: int,
|
def relocate_up(self, source: RSFormCached, destination: RSFormCached,
|
||||||
item_ids: list[int]) -> list[Constituenta]:
|
items: list[Constituenta]) -> list[Constituenta]:
|
||||||
''' Move list of Constituents upstream to destination Schema. '''
|
''' Move list of Constituents upstream to destination Schema. '''
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
|
self.cache.insert_schema(source)
|
||||||
|
self.cache.insert_schema(destination)
|
||||||
|
|
||||||
source = self.context.get_schema(sourceID)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
destination = self.context.get_schema(destinationID)
|
|
||||||
|
|
||||||
operation = self.cache.get_operation(sourceID)
|
|
||||||
alias_mapping: dict[str, str] = {}
|
alias_mapping: dict[str, str] = {}
|
||||||
for item in self.cache.inheritance[operation.pk]:
|
for item in self.cache.inheritance[operation.pk]:
|
||||||
if item.parent_id in destination.cache.by_id:
|
if item.parent_id in destination.cache.by_id:
|
||||||
|
|
@ -238,27 +237,27 @@ class OperationSchemaCached:
|
||||||
destination_cst = destination.cache.by_id[item.parent_id]
|
destination_cst = destination.cache.by_id[item.parent_id]
|
||||||
alias_mapping[source_cst.alias] = destination_cst.alias
|
alias_mapping[source_cst.alias] = destination_cst.alias
|
||||||
|
|
||||||
new_items = destination.insert_from(sourceID, item_ids, alias_mapping)
|
new_items = destination.insert_copy(items, initial_mapping=alias_mapping)
|
||||||
for (cst, new_cst) in new_items:
|
for index, cst in enumerate(new_items):
|
||||||
new_inheritance = Inheritance.objects.create(
|
new_inheritance = Inheritance.objects.create(
|
||||||
operation=operation,
|
operation=operation,
|
||||||
child=cst,
|
child=items[index],
|
||||||
parent=new_cst
|
parent=cst
|
||||||
)
|
)
|
||||||
self.cache.insert_inheritance(new_inheritance)
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
new_constituents = [item[1] for item in new_items]
|
self.after_create_cst(destination, new_items, exclude=[operation.pk])
|
||||||
self.after_create_cst(destinationID, new_constituents, exclude=[operation.pk])
|
destination.model.save(update_fields=['time_update'])
|
||||||
return new_constituents
|
return new_items
|
||||||
|
|
||||||
def after_create_cst(
|
def after_create_cst(
|
||||||
self, sourceID: int,
|
self, source: RSFormCached,
|
||||||
cst_list: list[Constituenta],
|
cst_list: list[Constituenta],
|
||||||
exclude: Optional[list[int]] = None
|
exclude: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Trigger cascade resolutions when new Constituenta is created. '''
|
''' Trigger cascade resolutions when new Constituenta is created. '''
|
||||||
source = self.context.get_schema(sourceID)
|
self.cache.insert_schema(source)
|
||||||
alias_mapping = create_dependant_mapping(source, cst_list)
|
alias_mapping = create_dependant_mapping(source, cst_list)
|
||||||
operation = self.cache.get_operation(source.pk)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
self.engine.on_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude)
|
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:
|
def after_change_cst_type(self, schemaID: int, target: int, new_type: CstType) -> None:
|
||||||
|
|
@ -266,10 +265,10 @@ class OperationSchemaCached:
|
||||||
operation = self.cache.get_operation(schemaID)
|
operation = self.cache.get_operation(schemaID)
|
||||||
self.engine.on_change_cst_type(operation.pk, target, new_type)
|
self.engine.on_change_cst_type(operation.pk, target, new_type)
|
||||||
|
|
||||||
def after_update_cst(self, sourceID: int, target: int, data: dict, old_data: dict) -> None:
|
def after_update_cst(self, source: RSFormCached, target: int, data: dict, old_data: dict) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
''' Trigger cascade resolutions when Constituenta data is changed. '''
|
||||||
operation = self.cache.get_operation(sourceID)
|
self.cache.insert_schema(source)
|
||||||
source = self.context.get_schema(sourceID)
|
operation = self.cache.get_operation(source.model.pk)
|
||||||
depend_aliases = extract_data_references(data, old_data)
|
depend_aliases = extract_data_references(data, old_data)
|
||||||
alias_mapping: CstMapping = {}
|
alias_mapping: CstMapping = {}
|
||||||
for alias in depend_aliases:
|
for alias in depend_aliases:
|
||||||
|
|
@ -299,18 +298,18 @@ class OperationSchemaCached:
|
||||||
if target.result_id is None:
|
if target.result_id is None:
|
||||||
return
|
return
|
||||||
for argument in arguments:
|
for argument in arguments:
|
||||||
parent_schema = self.cache.get_result(argument)
|
parent_schema = self.cache.get_schema(argument)
|
||||||
if parent_schema:
|
if parent_schema is not None:
|
||||||
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
|
self.engine.delete_inherited(target.pk, [cst.pk for cst in parent_schema.cache.constituents])
|
||||||
|
|
||||||
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
def after_create_arguments(self, target: Operation, arguments: list[Operation]) -> None:
|
||||||
''' Trigger cascade resolutions after arguments are created. '''
|
''' Trigger cascade resolutions after arguments are created. '''
|
||||||
schema = self.cache.get_result(target)
|
schema = self.cache.get_schema(target)
|
||||||
if not schema:
|
if schema is None:
|
||||||
return
|
return
|
||||||
for argument in arguments:
|
for argument in arguments:
|
||||||
parent_schema = self.cache.get_result(argument)
|
parent_schema = self.cache.get_schema(argument)
|
||||||
if not parent_schema:
|
if parent_schema is None:
|
||||||
continue
|
continue
|
||||||
self.engine.inherit_cst(
|
self.engine.inherit_cst(
|
||||||
target_operation=target.pk,
|
target_operation=target.pk,
|
||||||
|
|
@ -319,16 +318,16 @@ class OperationSchemaCached:
|
||||||
mapping={}
|
mapping={}
|
||||||
)
|
)
|
||||||
|
|
||||||
def after_create_attribution(self, schemaID: int, attributions: list[Attribution],
|
def after_create_attribution(self, schemaID: int, associations: list[Attribution],
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when Attribution is created. '''
|
''' Trigger cascade resolutions when Attribution is created. '''
|
||||||
operation = self.cache.get_operation(schemaID)
|
operation = self.cache.get_operation(schemaID)
|
||||||
self.engine.on_inherit_attribution(operation.pk, attributions, exclude)
|
self.engine.on_inherit_attribution(operation.pk, associations, exclude)
|
||||||
|
|
||||||
def before_delete_attribution(self, schemaID: int, attributions: list[Attribution]) -> None:
|
def before_delete_attribution(self, schemaID: int, associations: list[Attribution]) -> None:
|
||||||
''' Trigger cascade resolutions when Attribution is deleted. '''
|
''' Trigger cascade resolutions when Attribution is deleted. '''
|
||||||
operation = self.cache.get_operation(schemaID)
|
operation = self.cache.get_operation(schemaID)
|
||||||
self.engine.on_delete_attribution(operation.pk, attributions)
|
self.engine.on_delete_attribution(operation.pk, associations)
|
||||||
|
|
||||||
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
def _on_add_substitutions(self, schema: Optional[RSFormCached], added: list[Substitution]) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta substitution is added. '''
|
''' Trigger cascade resolutions when Constituenta substitution is added. '''
|
||||||
|
|
@ -348,7 +347,7 @@ class OperationSchemaCached:
|
||||||
original_cst = schema.cache.by_id[original_id]
|
original_cst = schema.cache.by_id[original_id]
|
||||||
substitution_cst = schema.cache.by_id[substitution_id]
|
substitution_cst = schema.cache.by_id[substitution_id]
|
||||||
cst_mapping.append((original_cst, substitution_cst))
|
cst_mapping.append((original_cst, substitution_cst))
|
||||||
self.before_substitute(schema.pk, cst_mapping)
|
self.before_substitute(schema.model.pk, cst_mapping)
|
||||||
schema.substitute(cst_mapping)
|
schema.substitute(cst_mapping)
|
||||||
for sub in added:
|
for sub in added:
|
||||||
self.cache.insert_substitution(sub)
|
self.cache.insert_substitution(sub)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from apps.rsform.models import RSFormCached
|
||||||
from .Argument import Argument
|
from .Argument import Argument
|
||||||
from .Inheritance import Inheritance
|
from .Inheritance import Inheritance
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation, OperationType
|
||||||
from .PropagationContext import PropagationContext
|
|
||||||
from .Replica import Replica
|
from .Replica import Replica
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
|
||||||
|
|
@ -16,9 +15,10 @@ from .Substitution import Substitution
|
||||||
class OssCache:
|
class OssCache:
|
||||||
''' Cache for OSS data. '''
|
''' Cache for OSS data. '''
|
||||||
|
|
||||||
def __init__(self, item_id: int, context: PropagationContext) -> None:
|
def __init__(self, item_id: int):
|
||||||
self._item_id = item_id
|
self._item_id = item_id
|
||||||
self._context = context
|
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.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.operation_by_id = {operation.pk: operation for operation in self.operations}
|
||||||
|
|
@ -60,11 +60,27 @@ class OssCache:
|
||||||
'operation_id', 'parent_id', 'child_id'):
|
'operation_id', 'parent_id', 'child_id'):
|
||||||
self.inheritance[item.operation_id].append(item)
|
self.inheritance[item.operation_id].append(item)
|
||||||
|
|
||||||
def get_result(self, operation: Operation) -> Optional[RSFormCached]:
|
def get_schema(self, operation: Operation) -> Optional[RSFormCached]:
|
||||||
''' Get schema by Operation. '''
|
''' Get schema by Operation. '''
|
||||||
if operation.result_id is None:
|
if operation.result_id is None:
|
||||||
return None
|
return None
|
||||||
return self._context.get_schema(operation.result_id)
|
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:
|
def get_operation(self, schemaID: int) -> Operation:
|
||||||
''' Get operation by schema. '''
|
''' Get operation by schema. '''
|
||||||
|
|
@ -95,15 +111,11 @@ class OssCache:
|
||||||
return self.get_inheritor(sub.substitution_id, operation)
|
return self.get_inheritor(sub.substitution_id, operation)
|
||||||
return self.get_inheritor(parent_cst, operation)
|
return self.get_inheritor(parent_cst, operation)
|
||||||
|
|
||||||
def get_substitution_partners(self, cst: int, operation: int) -> list[int]:
|
def insert_schema(self, schema: RSFormCached) -> None:
|
||||||
''' Get originals or substitutes for target constituent in target operation. '''
|
''' Insert new schema. '''
|
||||||
result = []
|
if not self._schema_by_id.get(schema.model.pk):
|
||||||
for sub in self.substitutions[operation]:
|
schema.cache.ensure_loaded()
|
||||||
if sub.original_id == cst:
|
self._insert_new(schema)
|
||||||
result.append(sub.substitution_id)
|
|
||||||
elif sub.substitution_id == cst:
|
|
||||||
result.append(sub.original_id)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def insert_argument(self, argument: Argument) -> None:
|
def insert_argument(self, argument: Argument) -> None:
|
||||||
''' Insert new argument. '''
|
''' Insert new argument. '''
|
||||||
|
|
@ -133,12 +145,19 @@ class OssCache:
|
||||||
for item in inherit_to_delete:
|
for item in inherit_to_delete:
|
||||||
self.inheritance[operation].remove(item)
|
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:
|
def remove_operation(self, operation: int) -> None:
|
||||||
''' Remove operation from cache. '''
|
''' Remove operation from cache. '''
|
||||||
target = self.operation_by_id[operation]
|
target = self.operation_by_id[operation]
|
||||||
self.graph.remove_node(operation)
|
self.graph.remove_node(operation)
|
||||||
self.extend_graph.remove_node(operation)
|
self.extend_graph.remove_node(operation)
|
||||||
self._context.invalidate(target.result_id)
|
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])
|
self.operations.remove(self.operation_by_id[operation])
|
||||||
del self.operation_by_id[operation]
|
del self.operation_by_id[operation]
|
||||||
if operation in self.replica_original:
|
if operation in self.replica_original:
|
||||||
|
|
@ -163,3 +182,7 @@ class OssCache:
|
||||||
def remove_inheritance(self, target: Inheritance) -> None:
|
def remove_inheritance(self, target: Inheritance) -> None:
|
||||||
''' Remove inheritance from cache. '''
|
''' Remove inheritance from cache. '''
|
||||||
self.inheritance[target.operation_id].remove(target)
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
''' Models: Propagation context. '''
|
|
||||||
|
|
||||||
from apps.rsform.models import RSFormCached
|
|
||||||
|
|
||||||
|
|
||||||
class PropagationContext:
|
|
||||||
''' Propagation context. '''
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._cache: dict[int, RSFormCached] = {}
|
|
||||||
|
|
||||||
def get_schema(self, item_id: int) -> RSFormCached:
|
|
||||||
''' Get schema by ID. '''
|
|
||||||
if item_id not in self._cache:
|
|
||||||
schema = RSFormCached(item_id)
|
|
||||||
schema.cache.ensure_loaded()
|
|
||||||
self._cache[item_id] = schema
|
|
||||||
return self._cache[item_id]
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
''' Clear cache. '''
|
|
||||||
self._cache = {}
|
|
||||||
|
|
||||||
def invalidate(self, item_id: int | None) -> None:
|
|
||||||
''' Invalidate schema by ID. '''
|
|
||||||
if item_id in self._cache:
|
|
||||||
del self._cache[item_id]
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
''' Models: Change propagation engine. '''
|
''' Models: Change propagation engine. '''
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models import Q
|
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
|
from apps.rsform.models import INSERT_LAST, Attribution, Constituenta, CstType, RSFormCached
|
||||||
|
|
||||||
from .Inheritance import Inheritance
|
from .Inheritance import Inheritance
|
||||||
from .Operation import Operation
|
from .Operation import Operation
|
||||||
from .OssCache import OssCache
|
from .OssCache import OssCache
|
||||||
from .PropagationContext import PropagationContext
|
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
from .utils import (
|
from .utils import (
|
||||||
CstMapping,
|
CstMapping,
|
||||||
|
|
@ -23,9 +21,8 @@ from .utils import (
|
||||||
class PropagationEngine:
|
class PropagationEngine:
|
||||||
''' OSS changes propagation engine. '''
|
''' OSS changes propagation engine. '''
|
||||||
|
|
||||||
def __init__(self, cache: OssCache, context: PropagationContext) -> None:
|
def __init__(self, cache: OssCache):
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
def on_change_cst_type(self, operation_id: int, cst_id: int, ctype: CstType) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
''' Trigger cascade resolutions when Constituenta type is changed. '''
|
||||||
|
|
@ -38,7 +35,7 @@ class PropagationEngine:
|
||||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||||
if successor_id is None:
|
if successor_id is None:
|
||||||
continue
|
continue
|
||||||
child_schema = self.cache.get_result(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
if child_schema is None:
|
if child_schema is None:
|
||||||
continue
|
continue
|
||||||
if child_schema.change_cst_type(successor_id, ctype):
|
if child_schema.change_cst_type(successor_id, ctype):
|
||||||
|
|
@ -70,7 +67,7 @@ class PropagationEngine:
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Execute inheritance of Constituenta. '''
|
''' Execute inheritance of Constituenta. '''
|
||||||
operation = self.cache.operation_by_id[target_operation]
|
operation = self.cache.operation_by_id[target_operation]
|
||||||
destination = self.cache.get_result(operation)
|
destination = self.cache.get_schema(operation)
|
||||||
if destination is None:
|
if destination is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -79,11 +76,11 @@ class PropagationEngine:
|
||||||
alias_mapping = cst_mapping_to_alias(new_mapping)
|
alias_mapping = cst_mapping_to_alias(new_mapping)
|
||||||
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
insert_where = self._determine_insert_position(items[0].pk, operation, source, destination)
|
||||||
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
new_cst_list = destination.insert_copy(items, insert_where, alias_mapping)
|
||||||
for (cst, new_cst) in zip(items, new_cst_list):
|
for index, cst in enumerate(new_cst_list):
|
||||||
new_inheritance = Inheritance.objects.create(
|
new_inheritance = Inheritance.objects.create(
|
||||||
operation=operation,
|
operation=operation,
|
||||||
child=new_cst,
|
child=cst,
|
||||||
parent=cst
|
parent=items[index]
|
||||||
)
|
)
|
||||||
self.cache.insert_inheritance(new_inheritance)
|
self.cache.insert_inheritance(new_inheritance)
|
||||||
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
new_mapping = {alias_mapping[alias]: cst for alias, cst in new_mapping.items()}
|
||||||
|
|
@ -91,7 +88,9 @@ class PropagationEngine:
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
||||||
def on_update_cst(
|
def on_update_cst(
|
||||||
self, operation: int, cst_id: int,
|
self,
|
||||||
|
operation: int,
|
||||||
|
cst_id: int,
|
||||||
data: dict, old_data: dict,
|
data: dict, old_data: dict,
|
||||||
mapping: CstMapping
|
mapping: CstMapping
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -105,7 +104,7 @@ class PropagationEngine:
|
||||||
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
successor_id = self.cache.get_inheritor(cst_id, child_id)
|
||||||
if successor_id is None:
|
if successor_id is None:
|
||||||
continue
|
continue
|
||||||
child_schema = self.cache.get_result(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
assert child_schema is not None
|
assert child_schema is not None
|
||||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
alias_mapping = cst_mapping_to_alias(new_mapping)
|
alias_mapping = cst_mapping_to_alias(new_mapping)
|
||||||
|
|
@ -127,49 +126,47 @@ class PropagationEngine:
|
||||||
mapping=new_mapping
|
mapping=new_mapping
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_inherit_attribution(
|
def on_inherit_attribution(self, operationID: int,
|
||||||
self, operationID: int,
|
items: list[Attribution],
|
||||||
items: list[Attribution],
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
exclude: Optional[list[int]] = None
|
|
||||||
) -> None:
|
|
||||||
''' Trigger cascade resolutions when Attribution is inherited. '''
|
''' Trigger cascade resolutions when Attribution is inherited. '''
|
||||||
children = self.cache.extend_graph.outputs[operationID]
|
children = self.cache.extend_graph.outputs[operationID]
|
||||||
if not children:
|
if not children:
|
||||||
return
|
return
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
if not exclude or child_id not in exclude:
|
if not exclude or child_id not in exclude:
|
||||||
self.inherit_attributions(child_id, items)
|
self.inherit_association(child_id, items)
|
||||||
|
|
||||||
def inherit_attributions(self, target: int, items: list[Attribution]) -> None:
|
def inherit_association(self, target: int, items: list[Attribution]) -> None:
|
||||||
''' Execute inheritance of Attributions. '''
|
''' Execute inheritance of Associations. '''
|
||||||
operation = self.cache.operation_by_id[target]
|
operation = self.cache.operation_by_id[target]
|
||||||
if operation.result is None or not items:
|
if operation.result is None or not items:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
|
|
||||||
existing_attributions = set(
|
existing_associations = set(
|
||||||
Attribution.objects.filter(
|
Attribution.objects.filter(
|
||||||
container__schema_id=operation.result_id,
|
container__schema_id=operation.result_id,
|
||||||
).values_list('container_id', 'attribute_id')
|
).values_list('container_id', 'attribute_id')
|
||||||
)
|
)
|
||||||
|
|
||||||
new_attributions: list[Attribution] = []
|
new_associations: list[Attribution] = []
|
||||||
for attrib in items:
|
for assoc in items:
|
||||||
new_container = self.cache.get_successor(attrib.container_id, target)
|
new_container = self.cache.get_inheritor(assoc.container_id, target)
|
||||||
new_attribute = self.cache.get_successor(attrib.attribute_id, target)
|
new_attribute = self.cache.get_inheritor(assoc.attribute_id, target)
|
||||||
if new_container is None or new_attribute is None \
|
if new_container is None or new_attribute is None \
|
||||||
or new_attribute == new_container \
|
or new_attribute == new_container \
|
||||||
or (new_container, new_attribute) in existing_attributions:
|
or (new_container, new_attribute) in existing_associations:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
new_attributions.append(Attribution(
|
new_associations.append(Attribution(
|
||||||
container_id=new_container,
|
container_id=new_container,
|
||||||
attribute_id=new_attribute
|
attribute_id=new_attribute
|
||||||
))
|
))
|
||||||
if new_attributions:
|
if new_associations:
|
||||||
new_attributions = Attribution.objects.bulk_create(new_attributions)
|
new_associations = Attribution.objects.bulk_create(new_associations)
|
||||||
self.on_inherit_attribution(target, new_attributions)
|
self.on_inherit_attribution(target, new_associations)
|
||||||
|
|
||||||
def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None:
|
def on_before_substitute(self, operationID: int, substitutions: CstSubstitution) -> None:
|
||||||
''' Trigger cascade resolutions when Constituenta substitution is executed. '''
|
''' Trigger cascade resolutions when Constituenta substitution is executed. '''
|
||||||
|
|
@ -179,7 +176,7 @@ class PropagationEngine:
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
child_schema = self.cache.get_result(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
if child_schema is None:
|
if child_schema is None:
|
||||||
continue
|
continue
|
||||||
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
new_substitutions = self._transform_substitutions(substitutions, child_id, child_schema)
|
||||||
|
|
@ -188,75 +185,57 @@ class PropagationEngine:
|
||||||
self.on_before_substitute(child_operation.pk, new_substitutions)
|
self.on_before_substitute(child_operation.pk, new_substitutions)
|
||||||
child_schema.substitute(new_substitutions)
|
child_schema.substitute(new_substitutions)
|
||||||
|
|
||||||
def on_delete_attribution(self, operationID: int, attributions: list[Attribution]) -> None:
|
def on_delete_attribution(self, operationID: int, associations: list[Attribution]) -> None:
|
||||||
''' Trigger cascade resolutions when Attribution is deleted. '''
|
''' Trigger cascade resolutions when Attribution is deleted. '''
|
||||||
children = self.cache.extend_graph.outputs[operationID]
|
children = self.cache.extend_graph.outputs[operationID]
|
||||||
if not children:
|
if not children:
|
||||||
return
|
return
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
self._delete_child_attributions(child_id, attributions)
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
def _delete_child_attributions(self, operationID: int, attributions: list[Attribution]) -> None:
|
if child_schema is None:
|
||||||
child_operation = self.cache.operation_by_id[operationID]
|
|
||||||
child_schema = self.cache.get_result(child_operation)
|
|
||||||
if child_schema is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
deleted: list[Attribution] = []
|
|
||||||
for attr in attributions:
|
|
||||||
new_container = self.cache.get_successor(attr.container_id, operationID)
|
|
||||||
new_attribute = self.cache.get_successor(attr.attribute_id, operationID)
|
|
||||||
if new_container is None or new_attribute is None:
|
|
||||||
continue
|
|
||||||
deleted_attr = Attribution.objects.filter(
|
|
||||||
container=new_container,
|
|
||||||
attribute=new_attribute
|
|
||||||
).first()
|
|
||||||
if not deleted_attr:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not self._has_alternative_attribution(operationID, attr.container_id, attr.attribute_id):
|
deleted: list[Attribution] = []
|
||||||
deleted.append(deleted_attr)
|
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()
|
||||||
|
|
||||||
if deleted:
|
def on_delete_inherited(self, operation: int, target: list[int]) -> None:
|
||||||
self.on_delete_attribution(operationID, deleted)
|
|
||||||
Attribution.objects.filter(pk__in=[attrib.pk for attrib in deleted]).delete()
|
|
||||||
|
|
||||||
def _has_alternative_attribution(self, operationID: int, container: int, attribute: int) -> bool:
|
|
||||||
''' Check if there is an alternative attribution among substitutions. '''
|
|
||||||
container_partners = self.cache.get_substitution_partners(container, operationID)
|
|
||||||
attribute_partners = self.cache.get_substitution_partners(attribute, operationID)
|
|
||||||
if not container_partners or not attribute_partners:
|
|
||||||
return False
|
|
||||||
return Attribution.objects.filter(container__in=container_partners, attribute__in=attribute_partners).exists()
|
|
||||||
|
|
||||||
def on_delete_inherited(self, operationID: int, target: list[int]) -> None:
|
|
||||||
''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
|
''' Trigger cascade resolutions when Constituenta inheritance is deleted. '''
|
||||||
children = self.cache.extend_graph.outputs[operationID]
|
children = self.cache.extend_graph.outputs[operation]
|
||||||
if not children:
|
if not children:
|
||||||
return
|
return
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
self.delete_inherited(child_id, target)
|
self.delete_inherited(child_id, target)
|
||||||
|
|
||||||
def delete_inherited(self, operationID: int, parents: list[int]) -> None:
|
def delete_inherited(self, operation_id: int, parent_ids: list[int]) -> None:
|
||||||
''' Execute deletion of Constituenta inheritance. '''
|
''' Execute deletion of Constituenta inheritance. '''
|
||||||
operation = self.cache.operation_by_id[operationID]
|
operation = self.cache.operation_by_id[operation_id]
|
||||||
schema = self.cache.get_result(operation)
|
schema = self.cache.get_schema(operation)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
return
|
return
|
||||||
self.undo_substitutions_cst(parents, operation, schema)
|
self.undo_substitutions_cst(parent_ids, operation, schema)
|
||||||
target_ids = self.cache.get_inheritors_list(parents, operationID)
|
target_ids = self.cache.get_inheritors_list(parent_ids, operation_id)
|
||||||
self.on_delete_inherited(operationID, target_ids)
|
self.on_delete_inherited(operation_id, target_ids)
|
||||||
if target_ids:
|
if target_ids:
|
||||||
self.cache.remove_cst(operationID, target_ids)
|
self.cache.remove_cst(operation_id, target_ids)
|
||||||
schema.delete_cst(target_ids)
|
schema.delete_cst(target_ids)
|
||||||
|
|
||||||
def undo_substitutions_cst(
|
def undo_substitutions_cst(self, target_ids: list[int], operation: Operation, schema: RSFormCached) -> None:
|
||||||
self, target_ids: list[int],
|
|
||||||
operation: Operation, schema: RSFormCached
|
|
||||||
) -> None:
|
|
||||||
''' Undo substitutions for Constituents. '''
|
''' Undo substitutions for Constituents. '''
|
||||||
to_process = []
|
to_process = []
|
||||||
for sub in self.cache.substitutions[operation.pk]:
|
for sub in self.cache.substitutions[operation.pk]:
|
||||||
|
|
@ -266,23 +245,17 @@ class PropagationEngine:
|
||||||
self.undo_substitution(schema, sub, target_ids)
|
self.undo_substitution(schema, sub, target_ids)
|
||||||
|
|
||||||
def undo_substitution(
|
def undo_substitution(
|
||||||
self, schema: RSFormCached, target: Substitution,
|
self,
|
||||||
ignore_parents: Optional[list[int]] = None
|
schema: RSFormCached,
|
||||||
|
target: Substitution,
|
||||||
|
ignore_parents: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Undo target substitution. '''
|
''' Undo target substitution. '''
|
||||||
if ignore_parents is None:
|
if ignore_parents is None:
|
||||||
ignore_parents = []
|
ignore_parents = []
|
||||||
operation_id = target.operation_id
|
operation_id = target.operation_id
|
||||||
|
original_schema = self.cache.get_schema_by_id(target.original.schema_id)
|
||||||
original_attributions = list(Attribution.objects.filter(
|
|
||||||
Q(container=target.original_id) |
|
|
||||||
Q(attribute=target.original_id)
|
|
||||||
))
|
|
||||||
if original_attributions:
|
|
||||||
self._delete_child_attributions(operation_id, original_attributions)
|
|
||||||
|
|
||||||
dependant = []
|
dependant = []
|
||||||
original_schema = self.context.get_schema(target.original.schema_id)
|
|
||||||
for cst_id in original_schema.get_dependant([target.original_id]):
|
for cst_id in original_schema.get_dependant([target.original_id]):
|
||||||
if cst_id not in ignore_parents:
|
if cst_id not in ignore_parents:
|
||||||
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
inheritor_id = self.cache.get_inheritor(cst_id, operation_id)
|
||||||
|
|
@ -297,9 +270,6 @@ class PropagationEngine:
|
||||||
full_cst = Constituenta.objects.get(pk=target.original_id)
|
full_cst = Constituenta.objects.get(pk=target.original_id)
|
||||||
cst_mapping = create_dependant_mapping(original_schema, [full_cst])
|
cst_mapping = create_dependant_mapping(original_schema, [full_cst])
|
||||||
self.inherit_cst(operation_id, original_schema, [full_cst], cst_mapping)
|
self.inherit_cst(operation_id, original_schema, [full_cst], cst_mapping)
|
||||||
if original_attributions:
|
|
||||||
self.inherit_attributions(operation_id, original_attributions)
|
|
||||||
|
|
||||||
new_original_id = self.cache.get_inheritor(target.original_id, operation_id)
|
new_original_id = self.cache.get_inheritor(target.original_id, operation_id)
|
||||||
assert new_original_id is not None
|
assert new_original_id is not None
|
||||||
new_original = schema.cache.by_id[new_original_id]
|
new_original = schema.cache.by_id[new_original_id]
|
||||||
|
|
@ -311,13 +281,12 @@ class PropagationEngine:
|
||||||
mapping = {substitution_inheritor.alias: new_original}
|
mapping = {substitution_inheritor.alias: new_original}
|
||||||
self._on_partial_mapping(mapping, dependant, operation_id, schema)
|
self._on_partial_mapping(mapping, dependant, operation_id, schema)
|
||||||
|
|
||||||
|
|
||||||
def _determine_insert_position(
|
def _determine_insert_position(
|
||||||
self, prototype_id: int,
|
self, prototype_id: int,
|
||||||
operation: Operation,
|
operation: Operation,
|
||||||
source: RSFormCached,
|
source: RSFormCached,
|
||||||
destination: RSFormCached
|
destination: RSFormCached
|
||||||
) -> Optional[int]:
|
) -> int:
|
||||||
''' Determine insert_after for new constituenta. '''
|
''' Determine insert_after for new constituenta. '''
|
||||||
prototype = source.cache.by_id[prototype_id]
|
prototype = source.cache.by_id[prototype_id]
|
||||||
prototype_index = source.cache.constituents.index(prototype)
|
prototype_index = source.cache.constituents.index(prototype)
|
||||||
|
|
@ -326,7 +295,7 @@ class PropagationEngine:
|
||||||
prev_cst = source.cache.constituents[prototype_index - 1]
|
prev_cst = source.cache.constituents[prototype_index - 1]
|
||||||
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
inherited_prev_id = self.cache.get_successor(prev_cst.pk, operation.pk)
|
||||||
if inherited_prev_id is None:
|
if inherited_prev_id is None:
|
||||||
return None
|
return INSERT_LAST
|
||||||
prev_cst = destination.cache.by_id[inherited_prev_id]
|
prev_cst = destination.cache.by_id[inherited_prev_id]
|
||||||
prev_index = destination.cache.constituents.index(prev_cst)
|
prev_index = destination.cache.constituents.index(prev_cst)
|
||||||
return prev_index + 1
|
return prev_index + 1
|
||||||
|
|
@ -349,7 +318,8 @@ class PropagationEngine:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _transform_substitutions(
|
def _transform_substitutions(
|
||||||
self, target: CstSubstitution,
|
self,
|
||||||
|
target: CstSubstitution,
|
||||||
operation: int,
|
operation: int,
|
||||||
schema: RSFormCached
|
schema: RSFormCached
|
||||||
) -> CstSubstitution:
|
) -> CstSubstitution:
|
||||||
|
|
@ -389,7 +359,8 @@ class PropagationEngine:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _on_partial_mapping(
|
def _on_partial_mapping(
|
||||||
self, mapping: CstMapping,
|
self,
|
||||||
|
mapping: CstMapping,
|
||||||
target: list[int],
|
target: list[int],
|
||||||
operation: int,
|
operation: int,
|
||||||
schema: RSFormCached
|
schema: RSFormCached
|
||||||
|
|
@ -403,7 +374,7 @@ class PropagationEngine:
|
||||||
self.cache.ensure_loaded_subs()
|
self.cache.ensure_loaded_subs()
|
||||||
for child_id in children:
|
for child_id in children:
|
||||||
child_operation = self.cache.operation_by_id[child_id]
|
child_operation = self.cache.operation_by_id[child_id]
|
||||||
child_schema = self.cache.get_result(child_operation)
|
child_schema = self.cache.get_schema(child_operation)
|
||||||
if child_schema is None:
|
if child_schema is None:
|
||||||
continue
|
continue
|
||||||
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
new_mapping = self._transform_mapping(mapping, child_operation, child_schema)
|
||||||
|
|
|
||||||
|
|
@ -1,110 +1,101 @@
|
||||||
''' Models: Change propagation facade - managing all changes in OSS. '''
|
''' Models: Change propagation facade - managing all changes in OSS. '''
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from apps.library.models import LibraryItem
|
from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
|
from apps.rsform.models import Attribution, Constituenta, CstType, RSFormCached
|
||||||
|
|
||||||
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
from .OperationSchemaCached import CstSubstitution, OperationSchemaCached
|
||||||
from .PropagationContext import PropagationContext
|
|
||||||
|
|
||||||
|
|
||||||
def _get_oss_hosts(schemaID: int) -> list[int]:
|
def _get_oss_hosts(schemaID: int) -> list[LibraryItem]:
|
||||||
''' Get all hosts for schema. '''
|
''' Get all hosts for LibraryItem. '''
|
||||||
return list(LibraryItem.objects.filter(operations__result_id=schemaID).distinct().values_list('pk', flat=True))
|
return list(LibraryItem.objects.filter(operations__result_id=schemaID).only('pk').distinct())
|
||||||
|
|
||||||
|
|
||||||
class PropagationFacade:
|
class PropagationFacade:
|
||||||
''' Change propagation API. '''
|
''' Change propagation API. '''
|
||||||
|
|
||||||
def __init__(self) -> None:
|
@staticmethod
|
||||||
self._context = PropagationContext()
|
def after_create_cst(source: RSFormCached, new_cst: list[Constituenta],
|
||||||
self._oss: dict[int, OperationSchemaCached] = {}
|
|
||||||
|
|
||||||
def get_oss(self, schemaID: int) -> OperationSchemaCached:
|
|
||||||
''' Get OperationSchemaCached for schemaID. '''
|
|
||||||
if schemaID not in self._oss:
|
|
||||||
self._oss[schemaID] = OperationSchemaCached(schemaID, self._context)
|
|
||||||
return self._oss[schemaID]
|
|
||||||
|
|
||||||
def get_schema(self, schemaID: int) -> RSFormCached:
|
|
||||||
''' Get RSFormCached for schemaID. '''
|
|
||||||
return self._context.get_schema(schemaID)
|
|
||||||
|
|
||||||
def after_create_cst(self, new_cst: list[Constituenta],
|
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when new constituenta is created. '''
|
''' Trigger cascade resolutions when new constituenta is created. '''
|
||||||
if not new_cst:
|
hosts = _get_oss_hosts(source.model.pk)
|
||||||
return
|
|
||||||
source = new_cst[0].schema_id
|
|
||||||
hosts = _get_oss_hosts(source)
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).after_create_cst(source, new_cst)
|
OperationSchemaCached(host).after_create_cst(source, new_cst)
|
||||||
|
|
||||||
def after_change_cst_type(self, sourceID: int, target: int, new_type: CstType,
|
@staticmethod
|
||||||
|
def after_change_cst_type(sourceID: int, target: int, new_type: CstType,
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta type is changed. '''
|
''' Trigger cascade resolutions when constituenta type is changed. '''
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).after_change_cst_type(sourceID, target, new_type)
|
OperationSchemaCached(host).after_change_cst_type(sourceID, target, new_type)
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
@staticmethod
|
||||||
def after_update_cst(
|
def after_update_cst(
|
||||||
self, sourceID: int, target: int,
|
source: RSFormCached,
|
||||||
data: dict, old_data: dict,
|
target: int,
|
||||||
|
data: dict,
|
||||||
|
old_data: dict,
|
||||||
exclude: Optional[list[int]] = None
|
exclude: Optional[list[int]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
''' Trigger cascade resolutions when constituenta data is changed. '''
|
''' Trigger cascade resolutions when constituenta data is changed. '''
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(source.model.pk)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).after_update_cst(sourceID, target, data, old_data)
|
OperationSchemaCached(host).after_update_cst(source, target, data, old_data)
|
||||||
|
|
||||||
def before_delete_cst(self, sourceID: int, target: list[int],
|
@staticmethod
|
||||||
|
def before_delete_cst(sourceID: int, target: list[int],
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before constituents are deleted. '''
|
''' Trigger cascade resolutions before constituents are deleted. '''
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).before_delete_cst(sourceID, target)
|
OperationSchemaCached(host).before_delete_cst(sourceID, target)
|
||||||
|
|
||||||
def before_substitute(self, sourceID: int, substitutions: CstSubstitution,
|
@staticmethod
|
||||||
|
def before_substitute(sourceID: int, substitutions: CstSubstitution,
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before constituents are substituted. '''
|
''' Trigger cascade resolutions before constituents are substituted. '''
|
||||||
if not substitutions:
|
if not substitutions:
|
||||||
return
|
return
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).before_substitute(sourceID, substitutions)
|
OperationSchemaCached(host).before_substitute(sourceID, substitutions)
|
||||||
|
|
||||||
def before_delete_schema(self, target: int, exclude: Optional[list[int]] = None) -> None:
|
@staticmethod
|
||||||
|
def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before schema is deleted. '''
|
''' Trigger cascade resolutions before schema is deleted. '''
|
||||||
hosts = _get_oss_hosts(target)
|
if item.item_type != LibraryItemType.RSFORM:
|
||||||
|
return
|
||||||
|
hosts = _get_oss_hosts(item.pk)
|
||||||
if not hosts:
|
if not hosts:
|
||||||
return
|
return
|
||||||
|
|
||||||
ids = list(Constituenta.objects.filter(schema_id=target).order_by('order').values_list('pk', flat=True))
|
ids = list(Constituenta.objects.filter(schema=item).order_by('order').values_list('pk', flat=True))
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).before_delete_cst(target, ids)
|
OperationSchemaCached(host).before_delete_cst(item.pk, ids)
|
||||||
del self._oss[host]
|
|
||||||
|
|
||||||
def after_create_attribution(self, sourceID: int,
|
@staticmethod
|
||||||
attributions: list[Attribution],
|
def after_create_attribution(sourceID: int, associations: list[Attribution],
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions when Attribution is created. '''
|
''' Trigger cascade resolutions when Attribution is created. '''
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).after_create_attribution(sourceID, attributions)
|
OperationSchemaCached(host).after_create_attribution(sourceID, associations)
|
||||||
|
|
||||||
def before_delete_attribution(self, sourceID: int,
|
@staticmethod
|
||||||
attributions: list[Attribution],
|
def before_delete_attribution(sourceID: int,
|
||||||
|
associations: list[Attribution],
|
||||||
exclude: Optional[list[int]] = None) -> None:
|
exclude: Optional[list[int]] = None) -> None:
|
||||||
''' Trigger cascade resolutions before Attribution is deleted. '''
|
''' Trigger cascade resolutions before Attribution is deleted. '''
|
||||||
hosts = _get_oss_hosts(sourceID)
|
hosts = _get_oss_hosts(sourceID)
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if exclude is None or host not in exclude:
|
if exclude is None or host.pk not in exclude:
|
||||||
self.get_oss(host).before_delete_attribution(sourceID, attributions)
|
OperationSchemaCached(host).before_delete_attribution(sourceID, associations)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from .Layout import Layout
|
||||||
from .Operation import Operation, OperationType
|
from .Operation import Operation, OperationType
|
||||||
from .OperationSchema import OperationSchema
|
from .OperationSchema import OperationSchema
|
||||||
from .OperationSchemaCached import OperationSchemaCached
|
from .OperationSchemaCached import OperationSchemaCached
|
||||||
from .PropagationContext import PropagationContext
|
|
||||||
from .PropagationFacade import PropagationFacade
|
from .PropagationFacade import PropagationFacade
|
||||||
from .Replica import Replica
|
from .Replica import Replica
|
||||||
from .Substitution import Substitution
|
from .Substitution import Substitution
|
||||||
|
|
|
||||||
|
|
@ -602,13 +602,15 @@ class RelocateConstituentsSerializer(StrictSerializer):
|
||||||
items = PKField(
|
items = PKField(
|
||||||
many=True,
|
many=True,
|
||||||
allow_empty=False,
|
allow_empty=False,
|
||||||
queryset=Constituenta.objects.all().only('schema_id')
|
queryset=Constituenta.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs['destination'] = attrs['destination'].id
|
attrs['destination'] = attrs['destination'].id
|
||||||
attrs['source'] = attrs['items'][0].schema_id
|
attrs['source'] = attrs['items'][0].schema_id
|
||||||
|
|
||||||
|
# TODO: check permissions for editing source and destination
|
||||||
|
|
||||||
if attrs['source'] == attrs['destination']:
|
if attrs['source'] == attrs['destination']:
|
||||||
raise serializers.ValidationError({
|
raise serializers.ValidationError({
|
||||||
'destination': msg.sourceEqualDestination()
|
'destination': msg.sourceEqualDestination()
|
||||||
|
|
@ -623,6 +625,15 @@ class RelocateConstituentsSerializer(StrictSerializer):
|
||||||
'items': msg.RelocatingInherited()
|
'items': msg.RelocatingInherited()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
oss = LibraryItem.objects \
|
||||||
|
.filter(operations__result_id=attrs['destination']) \
|
||||||
|
.filter(operations__result_id=attrs['source']).only('id')
|
||||||
|
if not oss.exists():
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'destination': msg.schemasNotConnected()
|
||||||
|
})
|
||||||
|
attrs['oss'] = oss[0].pk
|
||||||
|
|
||||||
if Argument.objects.filter(
|
if Argument.objects.filter(
|
||||||
operation__result_id=attrs['destination'],
|
operation__result_id=attrs['destination'],
|
||||||
argument__result_id=attrs['source']
|
argument__result_id=attrs['source']
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
''' Testing API: Change attributes of OSS and RSForms. '''
|
''' Testing API: Change attributes of OSS and RSForms. '''
|
||||||
from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LocationHead
|
||||||
from apps.oss.models import OperationSchema, OperationType
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
from apps.rsform.models import RSForm
|
from apps.rsform.models import RSForm
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
''' Testing API: Change constituents in OSS. '''
|
''' Testing API: Change constituents in OSS. '''
|
||||||
|
|
||||||
from apps.oss.models import OperationSchema, OperationType, PropagationFacade
|
from apps.oss.models import OperationSchema, OperationType
|
||||||
from apps.rsform.models import Attribution, Constituenta, CstType, RSForm
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -95,14 +95,14 @@ class TestChangeConstituents(EndpointTester):
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
@decl_endpoint('/api/rsforms/{schema}/create-cst', method='post')
|
||||||
def test_create_constituenta(self):
|
def test_create_constituenta(self):
|
||||||
data = {
|
data = {
|
||||||
'alias': 'X3',
|
'alias': 'X3',
|
||||||
'cst_type': CstType.BASE,
|
'cst_type': CstType.BASE,
|
||||||
'definition_formal': 'X4 = X5'
|
'definition_formal': 'X4 = X5'
|
||||||
}
|
}
|
||||||
response = self.executeCreated(data, item=self.ks1.model.pk)
|
response = self.executeCreated(data, schema=self.ks1.model.pk)
|
||||||
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
new_cst = Constituenta.objects.get(pk=response.data['new_cst']['id'])
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=new_cst.pk)
|
||||||
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
self.assertEqual(self.ks1.constituentsQ().count(), 3)
|
||||||
|
|
@ -112,7 +112,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
|
self.assertEqual(inherited_cst.definition_formal, 'X1 = X2')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/update-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/update-cst', method='patch')
|
||||||
def test_update_constituenta(self):
|
def test_update_constituenta(self):
|
||||||
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
|
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_raw='@{X1|sing,nomn}')
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -125,7 +125,7 @@ class TestChangeConstituents(EndpointTester):
|
||||||
'crucial': True,
|
'crucial': True,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.executeOK(data, item=self.ks1.model.pk)
|
response = self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks1X1.refresh_from_db()
|
self.ks1X1.refresh_from_db()
|
||||||
d2.refresh_from_db()
|
d2.refresh_from_db()
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
||||||
|
|
@ -142,10 +142,10 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
self.assertEqual(inherited_cst.definition_raw, r'@{X2|sing,datv}')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/delete-multiple-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
def test_delete_constituenta(self):
|
def test_delete_constituenta(self):
|
||||||
data = {'items': [self.ks2X1.pk]}
|
data = {'items': [self.ks2X1.pk]}
|
||||||
self.executeOK(data, item=self.ks2.model.pk)
|
response = self.executeOK(data, schema=self.ks2.model.pk)
|
||||||
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
inherited_cst = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
||||||
self.ks2D1.refresh_from_db()
|
self.ks2D1.refresh_from_db()
|
||||||
self.assertEqual(self.ks2.constituentsQ().count(), 1)
|
self.assertEqual(self.ks2.constituentsQ().count(), 1)
|
||||||
|
|
@ -154,164 +154,17 @@ class TestChangeConstituents(EndpointTester):
|
||||||
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
|
self.assertEqual(inherited_cst.definition_formal, r'DEL\DEL')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/substitute', method='patch')
|
||||||
def test_substitute(self):
|
def test_substitute(self):
|
||||||
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
|
d2 = self.ks3.insert_last('D2', cst_type=CstType.TERM, definition_formal=r'X1\X2\X3')
|
||||||
data = {'substitutions': [{
|
data = {'substitutions': [{
|
||||||
'original': self.ks1X1.pk,
|
'original': self.ks1X1.pk,
|
||||||
'substitution': self.ks1X2.pk
|
'substitution': self.ks1X2.pk
|
||||||
}]}
|
}]}
|
||||||
self.executeOK(data, item=self.ks1.model.pk)
|
self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks1X2.refresh_from_db()
|
self.ks1X2.refresh_from_db()
|
||||||
d2.refresh_from_db()
|
d2.refresh_from_db()
|
||||||
self.assertEqual(self.ks1.constituentsQ().count(), 1)
|
self.assertEqual(self.ks1.constituentsQ().count(), 1)
|
||||||
self.assertEqual(self.ks3.constituentsQ().count(), 4)
|
self.assertEqual(self.ks3.constituentsQ().count(), 4)
|
||||||
self.assertEqual(self.ks1X2.order, 0)
|
self.assertEqual(self.ks1X2.order, 0)
|
||||||
self.assertEqual(d2.definition_formal, r'X2\X2\X3')
|
self.assertEqual(d2.definition_formal, r'X2\X2\X3')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
|
||||||
def test_create_attribution(self):
|
|
||||||
x1_child = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
|
||||||
x2_child = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
|
||||||
|
|
||||||
data = {'container': x1_child.pk, 'attribute': x2_child.pk}
|
|
||||||
self.executeBadData(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
data = {'container': self.ks1X1.pk, 'attribute': self.ks1X2.pk}
|
|
||||||
self.executeCreated(data, item=self.ks1.model.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
ks2x1_child = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
data = {'container': x1_child.pk, 'attribute': ks2x1_child.pk}
|
|
||||||
self.executeCreated(data, item=self.ks3.model.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=ks2x1_child).exists())
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
|
||||||
def test_create_attribution_substitution(self):
|
|
||||||
self.operation3.result.delete()
|
|
||||||
self.owned.set_substitutions(self.operation3.pk, [{
|
|
||||||
'original': self.ks1X1,
|
|
||||||
'substitution': self.ks2X1
|
|
||||||
}])
|
|
||||||
self.owned.execute_operation(self.operation3)
|
|
||||||
self.operation3.refresh_from_db()
|
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
|
||||||
ks2x1_child = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
ks1x2_child = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
|
||||||
ks2d1_child = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
|
||||||
|
|
||||||
data = {'container': ks1x2_child.pk, 'attribute': ks2x1_child.pk}
|
|
||||||
self.executeBadData(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
data = {'container': ks2d1_child.pk, 'attribute': ks2x1_child.pk}
|
|
||||||
self.executeBadData(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
data = {'container': self.ks1X1.pk, 'attribute': self.ks1X2.pk}
|
|
||||||
self.executeCreated(data, item=self.ks1.model.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=ks2x1_child, attribute=ks1x2_child).exists())
|
|
||||||
|
|
||||||
self.executeBadData(data, item=self.ks1.model.pk)
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/delete-attribution', method='patch')
|
|
||||||
def test_delete_attribution(self):
|
|
||||||
attr = Attribution.objects.create(container=self.ks1X1, attribute=self.ks1X2)
|
|
||||||
PropagationFacade().after_create_attribution(self.ks1.model.pk, [attr])
|
|
||||||
x1_child = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
|
||||||
x2_child = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
data = {'container': x1_child.pk, 'attribute': x2_child.pk}
|
|
||||||
self.executeBadData(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
data = {'container': self.ks1X1.pk, 'attribute': self.ks1X2.pk}
|
|
||||||
self.executeOK(data, item=self.ks1.model.pk)
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
|
|
||||||
self.executeBadData(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/delete-attribution', method='patch')
|
|
||||||
def test_delete_attribution_diamond_right(self):
|
|
||||||
Attribution.objects.create(container=self.ks1X1, attribute=self.ks1X2)
|
|
||||||
Attribution.objects.create(container=self.ks2X1, attribute=self.ks2D1)
|
|
||||||
self.operation3.result.delete()
|
|
||||||
self.owned.set_substitutions(self.operation3.pk, [{
|
|
||||||
'original': self.ks1X1,
|
|
||||||
'substitution': self.ks2X1
|
|
||||||
}, {
|
|
||||||
'original': self.ks1X2,
|
|
||||||
'substitution': self.ks2D1
|
|
||||||
}])
|
|
||||||
self.owned.execute_operation(self.operation3)
|
|
||||||
self.operation3.refresh_from_db()
|
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
|
||||||
x1_child = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
x2_child = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
data = {'container': self.ks2X1.pk, 'attribute': self.ks2D1.pk}
|
|
||||||
self.executeOK(data, item=self.ks2.model.pk)
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks2X1, attribute=self.ks2D1).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
data = {'container': self.ks1X1.pk, 'attribute': self.ks1X2.pk}
|
|
||||||
self.executeOK(data, item=self.ks1.model.pk)
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/delete-attribution', method='patch')
|
|
||||||
def test_delete_attribution_diamond_left(self):
|
|
||||||
Attribution.objects.create(container=self.ks1X1, attribute=self.ks1X2)
|
|
||||||
Attribution.objects.create(container=self.ks2X1, attribute=self.ks2D1)
|
|
||||||
self.operation3.result.delete()
|
|
||||||
self.owned.set_substitutions(self.operation3.pk, [{
|
|
||||||
'original': self.ks1X1,
|
|
||||||
'substitution': self.ks2X1
|
|
||||||
}, {
|
|
||||||
'original': self.ks1X2,
|
|
||||||
'substitution': self.ks2D1
|
|
||||||
}])
|
|
||||||
self.owned.execute_operation(self.operation3)
|
|
||||||
self.operation3.refresh_from_db()
|
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
|
||||||
x1_child = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
x2_child = Constituenta.objects.get(as_child__parent_id=self.ks2D1.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
data = {'container': self.ks1X1.pk, 'attribute': self.ks1X2.pk}
|
|
||||||
self.executeOK(data, item=self.ks1.model.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks2X1, attribute=self.ks2D1).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
data = {'container': self.ks2X1.pk, 'attribute': self.ks2D1.pk}
|
|
||||||
self.executeOK(data, item=self.ks2.model.pk)
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks2X1, attribute=self.ks2D1).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=x1_child, attribute=x2_child).exists())
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/clear-attributions', method='patch')
|
|
||||||
def test_clear_attributions(self):
|
|
||||||
Attribution.objects.create(container=self.ks1X1, attribute=self.ks1X2)
|
|
||||||
self.operation3.result.delete()
|
|
||||||
self.owned.execute_operation(self.operation3)
|
|
||||||
self.operation3.refresh_from_db()
|
|
||||||
self.ks3 = RSForm(self.operation3.result)
|
|
||||||
ks1x1_child = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
|
||||||
ks1x2_child = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
|
||||||
ks2x1_child = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
Attribution.objects.create(container=ks1x1_child, attribute=ks2x1_child)
|
|
||||||
data = {'target': ks1x1_child.pk}
|
|
||||||
self.executeOK(data, item=self.ks3.model.pk)
|
|
||||||
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks1X1, attribute=self.ks1X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=ks1x1_child, attribute=ks1x2_child).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=ks1x1_child, attribute=ks2x1_child).exists())
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
''' Testing API: Change substitutions in OSS. '''
|
''' Testing API: Change substitutions in OSS. '''
|
||||||
|
|
||||||
from apps.oss.models import OperationSchema, OperationType
|
from apps.oss.models import OperationSchema, OperationType
|
||||||
from apps.rsform.models import Attribution, Constituenta, RSForm
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,7 +25,6 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
|
self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
|
||||||
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
|
self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
|
||||||
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
|
self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
|
||||||
Attribution.objects.create(container=self.ks1X1, attribute=self.ks1X2)
|
|
||||||
|
|
||||||
self.ks2 = RSForm.create(
|
self.ks2 = RSForm.create(
|
||||||
alias='KS2',
|
alias='KS2',
|
||||||
|
|
@ -39,7 +38,6 @@ class TestChangeOperations(EndpointTester):
|
||||||
definition_formal=r'X1',
|
definition_formal=r'X1',
|
||||||
convention='KS2S1'
|
convention='KS2S1'
|
||||||
)
|
)
|
||||||
Attribution.objects.create(container=self.ks2S1, attribute=self.ks2X1)
|
|
||||||
|
|
||||||
self.ks3 = RSForm.create(
|
self.ks3 = RSForm.create(
|
||||||
alias='KS3',
|
alias='KS3',
|
||||||
|
|
@ -82,7 +80,6 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.operation4.refresh_from_db()
|
self.operation4.refresh_from_db()
|
||||||
self.ks4 = RSForm(self.operation4.result)
|
self.ks4 = RSForm(self.operation4.result)
|
||||||
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
|
||||||
self.ks4X2 = Constituenta.objects.get(as_child__parent_id=self.ks2X1.pk)
|
|
||||||
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
|
self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
|
||||||
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
|
self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
|
||||||
self.ks4D2 = self.ks4.insert_last(
|
self.ks4D2 = self.ks4.insert_last(
|
||||||
|
|
@ -90,7 +87,6 @@ class TestChangeOperations(EndpointTester):
|
||||||
definition_formal=r'X1 X2 X3 S1 D1',
|
definition_formal=r'X1 X2 X3 S1 D1',
|
||||||
convention='KS4D2'
|
convention='KS4D2'
|
||||||
)
|
)
|
||||||
Attribution.objects.create(container=self.ks4S1, attribute=self.ks4D2)
|
|
||||||
|
|
||||||
self.operation5 = self.owned.create_operation(
|
self.operation5 = self.owned.create_operation(
|
||||||
alias='5',
|
alias='5',
|
||||||
|
|
@ -183,8 +179,8 @@ class TestChangeOperations(EndpointTester):
|
||||||
title='Test6',
|
title='Test6',
|
||||||
owner=self.user
|
owner=self.user
|
||||||
)
|
)
|
||||||
ks6.insert_last('X1', convention='KS6X1')
|
ks6X1 = ks6.insert_last('X1', convention='KS6X1')
|
||||||
ks6.insert_last('X2', convention='KS6X2')
|
ks6X2 = ks6.insert_last('X2', convention='KS6X2')
|
||||||
ks6D1 = ks6.insert_last('D1', definition_formal='X1 X2', convention='KS6D1')
|
ks6D1 = ks6.insert_last('D1', definition_formal='X1 X2', convention='KS6D1')
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -334,35 +330,6 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
|
self.assertEqual(self.ks5D4.definition_formal, r'D1 X2 X3 S1 D1 D2 D3')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
|
||||||
def test_change_substitutions_attribution(self):
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4X1).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4D2).exists())
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'target': self.operation4.pk,
|
|
||||||
'item_data': {
|
|
||||||
'alias': 'Test4 mod',
|
|
||||||
'title': 'Test title mod',
|
|
||||||
'description': 'Comment mod'
|
|
||||||
},
|
|
||||||
'layout': self.layout_data,
|
|
||||||
'arguments': [self.operation1.pk, self.operation2.pk],
|
|
||||||
'substitutions': []
|
|
||||||
}
|
|
||||||
|
|
||||||
self.executeOK(data, item=self.owned_id)
|
|
||||||
self.assertEqual(self.operation4.getQ_substitutions().count(), 0)
|
|
||||||
x3 = Constituenta.objects.get(as_child__parent_id=self.ks1X1.pk)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4X2).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=x3, attribute=self.ks4X2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4D2).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=x3, attribute=self.ks4D2).exists())
|
|
||||||
self.assertFalse(Attribution.objects.filter(container=self.ks4S1, attribute=self.ks4X1).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x3, attribute=self.ks4X1).exists())
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
|
||||||
def test_change_arguments(self):
|
def test_change_arguments(self):
|
||||||
data = {
|
data = {
|
||||||
|
|
@ -421,7 +388,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
self.assertEqual(self.ks5.constituentsQ().count(), 8)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
def test_relocate_constituents_up(self):
|
def test_relocate_constituents_up(self):
|
||||||
ks1_old_count = self.ks1.constituentsQ().count()
|
ks1_old_count = self.ks1.constituentsQ().count()
|
||||||
ks4_old_count = self.ks4.constituentsQ().count()
|
ks4_old_count = self.ks4.constituentsQ().count()
|
||||||
|
|
@ -441,7 +408,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'items': [ks6A1.pk]
|
'items': [ks6A1.pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data, item=self.owned_id)
|
self.executeOK(data)
|
||||||
ks6.model.refresh_from_db()
|
ks6.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
self.ks4.model.refresh_from_db()
|
self.ks4.model.refresh_from_db()
|
||||||
|
|
@ -451,7 +418,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1)
|
self.assertEqual(self.ks4.constituentsQ().count(), ks4_old_count + 1)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
def test_relocate_constituents_down(self):
|
def test_relocate_constituents_down(self):
|
||||||
ks1_old_count = self.ks1.constituentsQ().count()
|
ks1_old_count = self.ks1.constituentsQ().count()
|
||||||
ks4_old_count = self.ks4.constituentsQ().count()
|
ks4_old_count = self.ks4.constituentsQ().count()
|
||||||
|
|
@ -471,7 +438,7 @@ class TestChangeOperations(EndpointTester):
|
||||||
'items': [self.ks1X2.pk]
|
'items': [self.ks1X2.pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
self.executeOK(data, item=self.owned_id)
|
self.executeOK(data)
|
||||||
ks6.model.refresh_from_db()
|
ks6.model.refresh_from_db()
|
||||||
self.ks1.model.refresh_from_db()
|
self.ks1.model.refresh_from_db()
|
||||||
self.ks4.model.refresh_from_db()
|
self.ks4.model.refresh_from_db()
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ class ReferencePropagationTestCase(EndpointTester):
|
||||||
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
@decl_endpoint('/api/rsforms/{schema}/delete-multiple-cst', method='patch')
|
||||||
def test_delete_constituenta(self):
|
def test_delete_constituenta(self):
|
||||||
data = {'items': [self.ks1X1.pk]}
|
data = {'items': [self.ks1X1.pk]}
|
||||||
self.executeOK(data, schema=self.ks1.model.pk)
|
response = self.executeOK(data, schema=self.ks1.model.pk)
|
||||||
self.ks4D2.refresh_from_db()
|
self.ks4D2.refresh_from_db()
|
||||||
self.ks5D4.refresh_from_db()
|
self.ks5D4.refresh_from_db()
|
||||||
self.ks6D2.refresh_from_db()
|
self.ks6D2.refresh_from_db()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
''' Testing API: Change substitutions in OSS. '''
|
''' Testing API: Change substitutions in OSS. '''
|
||||||
|
|
||||||
from apps.oss.models import OperationSchema, OperationType
|
from apps.oss.models import OperationSchema, OperationType
|
||||||
from apps.rsform.models import Constituenta, RSForm
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
''' Testing API: Operation Schema - blocks manipulation. '''
|
''' Testing API: Operation Schema - blocks manipulation. '''
|
||||||
from apps.oss.models import OperationSchema, OperationType
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||||
|
from apps.oss.models import Operation, OperationSchema, OperationType
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
''' Testing API: Operation Schema - operations manipulation. '''
|
''' Testing API: Operation Schema - operations manipulation. '''
|
||||||
from apps.library.models import Editor, LibraryItem
|
from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType
|
||||||
from apps.oss.models import (
|
from apps.oss.models import Argument, Operation, OperationSchema, OperationType, Replica
|
||||||
Argument,
|
from apps.rsform.models import Constituenta, RSForm
|
||||||
Operation,
|
|
||||||
OperationSchema,
|
|
||||||
OperationType,
|
|
||||||
Replica
|
|
||||||
)
|
|
||||||
from apps.rsform.models import Attribution, RSForm
|
|
||||||
from shared.EndpointTester import EndpointTester, decl_endpoint
|
from shared.EndpointTester import EndpointTester, decl_endpoint
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -633,9 +627,6 @@ class TestOssOperations(EndpointTester):
|
||||||
title='Target',
|
title='Target',
|
||||||
owner=self.user
|
owner=self.user
|
||||||
)
|
)
|
||||||
x1 = target_ks.insert_last('X1')
|
|
||||||
x2 = target_ks.insert_last('X2')
|
|
||||||
Attribution.objects.create(container=x1, attribute=x2)
|
|
||||||
data = {
|
data = {
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'ImportedAlias',
|
'alias': 'ImportedAlias',
|
||||||
|
|
@ -658,30 +649,24 @@ class TestOssOperations(EndpointTester):
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0]
|
operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0]
|
||||||
new_item = LibraryItem.objects.get(pk=new_operation['result'])
|
schema = LibraryItem.objects.get(pk=new_operation['result'])
|
||||||
new_schema = RSForm(new_item)
|
|
||||||
attributions = Attribution.objects.filter(container__schema=new_item)
|
|
||||||
self.assertEqual(new_schema.constituentsQ().count(), target_ks.constituentsQ().count())
|
|
||||||
self.assertEqual(len(attributions), 1)
|
|
||||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
||||||
self.assertEqual(new_operation['description'], data['item_data']['description'])
|
self.assertEqual(new_operation['description'], data['item_data']['description'])
|
||||||
self.assertEqual(new_operation['operation_type'], OperationType.INPUT)
|
self.assertEqual(new_operation['operation_type'], OperationType.INPUT)
|
||||||
self.assertEqual(new_item.pk, target_ks.model.pk) # Not a clone
|
self.assertEqual(schema.pk, target_ks.model.pk) # Not a clone
|
||||||
self.assertEqual(operation_node['x'], data['position']['x'])
|
self.assertEqual(operation_node['x'], data['position']['x'])
|
||||||
self.assertEqual(operation_node['y'], data['position']['y'])
|
self.assertEqual(operation_node['y'], data['position']['y'])
|
||||||
self.assertEqual(operation_node['width'], data['position']['width'])
|
self.assertEqual(operation_node['width'], data['position']['width'])
|
||||||
self.assertEqual(operation_node['height'], data['position']['height'])
|
self.assertEqual(operation_node['height'], data['position']['height'])
|
||||||
self.assertEqual(new_item.visible, target_ks.model.visible)
|
self.assertEqual(schema.visible, target_ks.model.visible)
|
||||||
self.assertEqual(new_item.access_policy, target_ks.model.access_policy)
|
self.assertEqual(schema.access_policy, target_ks.model.access_policy)
|
||||||
self.assertEqual(new_item.location, target_ks.model.location)
|
self.assertEqual(schema.location, target_ks.model.location)
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
||||||
def test_import_schema_clone(self):
|
def test_import_schema_clone(self):
|
||||||
self.populateData()
|
self.populateData()
|
||||||
# Use ks2 as the source RSForm
|
# Use ks2 as the source RSForm
|
||||||
x3 = self.ks2.insert_last('X3')
|
|
||||||
Attribution.objects.create(container=self.ks2X1, attribute=x3)
|
|
||||||
data = {
|
data = {
|
||||||
'item_data': {
|
'item_data': {
|
||||||
'alias': 'ClonedAlias',
|
'alias': 'ClonedAlias',
|
||||||
|
|
@ -704,26 +689,22 @@ class TestOssOperations(EndpointTester):
|
||||||
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
|
||||||
layout = response.data['oss']['layout']
|
layout = response.data['oss']['layout']
|
||||||
operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0]
|
operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0]
|
||||||
new_item = LibraryItem.objects.get(pk=new_operation['result'])
|
schema = LibraryItem.objects.get(pk=new_operation['result'])
|
||||||
new_schema = RSForm(new_item)
|
|
||||||
attributions = Attribution.objects.filter(container__schema=new_item)
|
|
||||||
self.assertEqual(new_schema.constituentsQ().count(), self.ks2.constituentsQ().count())
|
|
||||||
self.assertEqual(len(attributions), 1)
|
|
||||||
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
self.assertEqual(new_operation['alias'], data['item_data']['alias'])
|
||||||
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
self.assertEqual(new_operation['title'], data['item_data']['title'])
|
||||||
self.assertEqual(new_operation['description'], data['item_data']['description'])
|
self.assertEqual(new_operation['description'], data['item_data']['description'])
|
||||||
self.assertEqual(new_operation['operation_type'], OperationType.INPUT)
|
self.assertEqual(new_operation['operation_type'], OperationType.INPUT)
|
||||||
self.assertNotEqual(new_item.pk, self.ks2.model.pk) # Should be a clone
|
self.assertNotEqual(schema.pk, self.ks2.model.pk) # Should be a clone
|
||||||
self.assertEqual(new_item.alias, data['item_data']['alias'])
|
self.assertEqual(schema.alias, data['item_data']['alias'])
|
||||||
self.assertEqual(new_item.title, data['item_data']['title'])
|
self.assertEqual(schema.title, data['item_data']['title'])
|
||||||
self.assertEqual(new_item.description, data['item_data']['description'])
|
self.assertEqual(schema.description, data['item_data']['description'])
|
||||||
self.assertEqual(operation_node['x'], data['position']['x'])
|
self.assertEqual(operation_node['x'], data['position']['x'])
|
||||||
self.assertEqual(operation_node['y'], data['position']['y'])
|
self.assertEqual(operation_node['y'], data['position']['y'])
|
||||||
self.assertEqual(operation_node['width'], data['position']['width'])
|
self.assertEqual(operation_node['width'], data['position']['width'])
|
||||||
self.assertEqual(operation_node['height'], data['position']['height'])
|
self.assertEqual(operation_node['height'], data['position']['height'])
|
||||||
self.assertEqual(new_item.visible, False)
|
self.assertEqual(schema.visible, False)
|
||||||
self.assertEqual(new_item.access_policy, self.owned.model.access_policy)
|
self.assertEqual(schema.access_policy, self.owned.model.access_policy)
|
||||||
self.assertEqual(new_item.location, self.owned.model.location)
|
self.assertEqual(schema.location, self.owned.model.location)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
@decl_endpoint('/api/oss/{item}/import-schema', method='post')
|
||||||
|
|
|
||||||
|
|
@ -220,9 +220,8 @@ class TestOssViewset(EndpointTester):
|
||||||
self.executeBadData(data)
|
self.executeBadData(data)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/oss/{item}/relocate-constituents', method='post')
|
@decl_endpoint('/api/oss/relocate-constituents', method='post')
|
||||||
def test_relocate_constituents(self):
|
def test_relocate_constituents(self):
|
||||||
self.set_params(item=self.owned_id)
|
|
||||||
self.populateData()
|
self.populateData()
|
||||||
self.ks1X2 = self.ks1.insert_last('X2', convention='test')
|
self.ks1X2 = self.ks1.insert_last('X2', convention='test')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.library.models import LibraryItem, LibraryItemType
|
from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from apps.library.serializers import LibraryItemSerializer
|
from apps.library.serializers import LibraryItemSerializer
|
||||||
from apps.rsform.models import Constituenta
|
from apps.rsform.models import Constituenta, RSFormCached
|
||||||
from apps.rsform.serializers import CstTargetSerializer
|
from apps.rsform.serializers import CstTargetSerializer
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared import permissions
|
from shared import permissions
|
||||||
|
|
@ -23,6 +23,27 @@ from .. import models as m
|
||||||
from .. import serializers as s
|
from .. import serializers as s
|
||||||
|
|
||||||
|
|
||||||
|
def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem:
|
||||||
|
''' Create clone of prototype schema for operation. '''
|
||||||
|
clone = deepcopy(prototype)
|
||||||
|
clone.pk = None
|
||||||
|
clone.owner = oss.owner
|
||||||
|
clone.title = operation.title
|
||||||
|
clone.alias = operation.alias
|
||||||
|
clone.description = operation.description
|
||||||
|
clone.visible = False
|
||||||
|
clone.read_only = False
|
||||||
|
clone.access_policy = oss.access_policy
|
||||||
|
clone.location = oss.location
|
||||||
|
clone.save()
|
||||||
|
for cst in Constituenta.objects.filter(schema_id=prototype.pk):
|
||||||
|
cst_copy = deepcopy(cst)
|
||||||
|
cst_copy.pk = None
|
||||||
|
cst_copy.schema = clone
|
||||||
|
cst_copy.save()
|
||||||
|
return clone
|
||||||
|
|
||||||
|
|
||||||
@extend_schema(tags=['OSS'])
|
@extend_schema(tags=['OSS'])
|
||||||
@extend_schema_view()
|
@extend_schema_view()
|
||||||
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
|
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
|
||||||
|
|
@ -291,7 +312,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
'height': position['height']
|
'height': position['height']
|
||||||
})
|
})
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
m.OperationSchema.create_input(item.pk, new_operation)
|
m.OperationSchema.create_input(item, new_operation)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -408,22 +429,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
|
|
||||||
if serializer.validated_data['clone_source']:
|
if serializer.validated_data['clone_source']:
|
||||||
prototype: LibraryItem = serializer.validated_data['source']
|
prototype: LibraryItem = serializer.validated_data['source']
|
||||||
|
new_operation.result = _create_clone(prototype, new_operation, item)
|
||||||
schema_clone = deepcopy(prototype)
|
|
||||||
schema_clone.pk = None
|
|
||||||
schema_clone.owner = item.owner
|
|
||||||
schema_clone.title = new_operation.title
|
|
||||||
schema_clone.alias = new_operation.alias
|
|
||||||
schema_clone.description = new_operation.description
|
|
||||||
schema_clone.visible = False
|
|
||||||
schema_clone.read_only = False
|
|
||||||
schema_clone.access_policy = item.access_policy
|
|
||||||
schema_clone.location = item.location
|
|
||||||
schema_clone.save()
|
|
||||||
|
|
||||||
m.PropagationFacade().get_schema(schema_clone.pk).insert_from(prototype.pk)
|
|
||||||
|
|
||||||
new_operation.result = schema_clone
|
|
||||||
new_operation.save(update_fields=["result"])
|
new_operation.save(update_fields=["result"])
|
||||||
|
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
@ -545,8 +551,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(item)
|
||||||
oss = propagation.get_oss(item.pk)
|
|
||||||
if 'layout' in serializer.validated_data:
|
if 'layout' in serializer.validated_data:
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
|
|
@ -601,13 +606,12 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(item)
|
||||||
oss = propagation.get_oss(item.pk)
|
|
||||||
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents'])
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
if old_schema is not None:
|
if old_schema is not None:
|
||||||
if serializer.validated_data['delete_schema']:
|
if serializer.validated_data['delete_schema']:
|
||||||
propagation.before_delete_schema(old_schema.pk)
|
m.PropagationFacade.before_delete_schema(old_schema)
|
||||||
old_schema.delete()
|
old_schema.delete()
|
||||||
elif old_schema.is_synced(item):
|
elif old_schema.is_synced(item):
|
||||||
old_schema.visible = True
|
old_schema.visible = True
|
||||||
|
|
@ -643,8 +647,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
layout = [x for x in layout if x['nodeID'] != 'o' + str(operation.pk)]
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(item)
|
||||||
oss = propagation.get_oss(item.pk)
|
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
oss.delete_replica(operation.pk, keep_connections, keep_constituents)
|
oss.delete_replica(operation.pk, keep_connections, keep_constituents)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
@ -684,13 +687,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
schema = m.OperationSchema.create_input(item.pk, operation)
|
schema = m.OperationSchema.create_input(item, operation)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data={
|
data={
|
||||||
'new_schema': LibraryItemSerializer(schema).data,
|
'new_schema': LibraryItemSerializer(schema.model).data,
|
||||||
'oss': s.OperationSchemaSerializer(item).data
|
'oss': s.OperationSchemaSerializer(item).data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -730,8 +733,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
old_schema = target_operation.result
|
old_schema = target_operation.result
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(item)
|
||||||
oss = propagation.get_oss(item.pk)
|
|
||||||
if old_schema is not None:
|
if old_schema is not None:
|
||||||
if old_schema.is_synced(item):
|
if old_schema.is_synced(item):
|
||||||
old_schema.visible = True
|
old_schema.visible = True
|
||||||
|
|
@ -774,8 +776,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
layout = serializer.validated_data['layout']
|
layout = serializer.validated_data['layout']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(item)
|
||||||
oss = propagation.get_oss(item.pk)
|
|
||||||
oss.execute_operation(operation)
|
oss.execute_operation(operation)
|
||||||
m.Layout.update_data(pk, layout)
|
m.Layout.update_data(pk, layout)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
@ -829,27 +830,24 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
|
||||||
c.HTTP_404_NOT_FOUND: None
|
c.HTTP_404_NOT_FOUND: None
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=['post'], url_path='relocate-constituents')
|
@action(detail=False, methods=['post'], url_path='relocate-constituents')
|
||||||
def relocate_constituents(self, request: Request, pk) -> Response:
|
def relocate_constituents(self, request: Request) -> Response:
|
||||||
''' Relocate constituents from one schema to another. '''
|
''' Relocate constituents from one schema to another. '''
|
||||||
item = self._get_item()
|
|
||||||
serializer = s.RelocateConstituentsSerializer(data=request.data)
|
serializer = s.RelocateConstituentsSerializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
ids = [cst.pk for cst in data['items']]
|
ids = [cst.pk for cst in data['items']]
|
||||||
destinationID = data['destination']
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = m.PropagationFacade()
|
oss = m.OperationSchemaCached(LibraryItem.objects.get(pk=data['oss']))
|
||||||
oss = propagation.get_oss(item.pk)
|
source = RSFormCached(LibraryItem.objects.get(pk=data['source']))
|
||||||
source = propagation.get_schema(data['source'])
|
destination = RSFormCached(LibraryItem.objects.get(pk=data['destination']))
|
||||||
if data['move_down']:
|
if data['move_down']:
|
||||||
oss.relocate_down(destinationID, ids)
|
oss.relocate_down(source, destination, ids)
|
||||||
propagation.before_delete_cst(source.pk, ids)
|
m.PropagationFacade.before_delete_cst(data['source'], ids)
|
||||||
source.delete_cst(ids)
|
source.delete_cst(ids)
|
||||||
else:
|
else:
|
||||||
new_items = oss.relocate_up(source.pk, destinationID, ids)
|
new_items = oss.relocate_up(source, destination, data['items'])
|
||||||
propagation.after_create_cst(new_items, exclude=[oss.pk])
|
m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk])
|
||||||
item.save(update_fields=['time_update'])
|
|
||||||
|
|
||||||
return Response(status=c.HTTP_200_OK)
|
return Response(status=c.HTTP_200_OK)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ ItemType = TypeVar("ItemType")
|
||||||
class Graph(Generic[ItemType]):
|
class Graph(Generic[ItemType]):
|
||||||
''' Directed graph. '''
|
''' Directed graph. '''
|
||||||
|
|
||||||
def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None) -> None:
|
def __init__(self, graph: Optional[dict[ItemType, list[ItemType]]] = None):
|
||||||
if graph is None:
|
if graph is None:
|
||||||
self.outputs: dict[ItemType, list[ItemType]] = {}
|
self.outputs: dict[ItemType, list[ItemType]] = {}
|
||||||
self.inputs: dict[ItemType, list[ItemType]] = {}
|
self.inputs: dict[ItemType, list[ItemType]] = {}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from .SemanticInfo import SemanticInfo
|
||||||
class OrderManager:
|
class OrderManager:
|
||||||
''' Ordering helper class '''
|
''' Ordering helper class '''
|
||||||
|
|
||||||
def __init__(self, schema: RSFormCached) -> None:
|
def __init__(self, schema: RSFormCached):
|
||||||
self._semantic = SemanticInfo(schema)
|
self._semantic = SemanticInfo(schema)
|
||||||
self._items = schema.cache.constituents
|
self._items = schema.cache.constituents
|
||||||
self._cst_by_ID = schema.cache.by_id
|
self._cst_by_ID = schema.cache.by_id
|
||||||
|
|
@ -22,11 +22,14 @@ class OrderManager:
|
||||||
self._fix_semantic_children()
|
self._fix_semantic_children()
|
||||||
self._override_order()
|
self._override_order()
|
||||||
|
|
||||||
|
def _fix_topological(self) -> None:
|
||||||
|
sorted_ids = self._semantic.graph.sort_stable([cst.pk for cst in self._items])
|
||||||
|
sorted_items = [next(cst for cst in self._items if cst.pk == id) for id in sorted_ids]
|
||||||
|
self._items = sorted_items
|
||||||
|
|
||||||
def _fix_kernel(self) -> None:
|
def _fix_kernel(self) -> None:
|
||||||
result = [cst for cst in self._items if cst.cst_type == CstType.BASE]
|
result = [cst for cst in self._items if cst.cst_type == CstType.BASE]
|
||||||
result = result + [cst for cst in self._items if cst.cst_type == CstType.CONSTANT]
|
result = result + [cst for cst in self._items if cst.cst_type == CstType.CONSTANT]
|
||||||
result = result + \
|
|
||||||
[cst for cst in self._items if result.count(cst) == 0 and len(self._semantic.graph.inputs[cst.pk]) == 0]
|
|
||||||
kernel = [
|
kernel = [
|
||||||
cst.pk for cst in self._items if
|
cst.pk for cst in self._items if
|
||||||
cst.cst_type in [CstType.STRUCTURED, CstType.AXIOM] or
|
cst.cst_type in [CstType.STRUCTURED, CstType.AXIOM] or
|
||||||
|
|
@ -37,11 +40,6 @@ class OrderManager:
|
||||||
result = result + [cst for cst in self._items if result.count(cst) == 0]
|
result = result + [cst for cst in self._items if result.count(cst) == 0]
|
||||||
self._items = result
|
self._items = result
|
||||||
|
|
||||||
def _fix_topological(self) -> None:
|
|
||||||
sorted_ids = self._semantic.graph.sort_stable([cst.pk for cst in self._items])
|
|
||||||
sorted_items = [next(cst for cst in self._items if cst.pk == id) for id in sorted_ids]
|
|
||||||
self._items = sorted_items
|
|
||||||
|
|
||||||
def _fix_semantic_children(self) -> None:
|
def _fix_semantic_children(self) -> None:
|
||||||
result: list[Constituenta] = []
|
result: list[Constituenta] = []
|
||||||
marked: set[Constituenta] = set()
|
marked: set[Constituenta] = set()
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ from shared import messages as msg
|
||||||
|
|
||||||
from ..graph import Graph
|
from ..graph import Graph
|
||||||
from .api_RSLanguage import get_type_prefix, guess_type
|
from .api_RSLanguage import get_type_prefix, guess_type
|
||||||
from .Attribution import Attribution
|
|
||||||
from .Constituenta import Constituenta, CstType, extract_entities, extract_globals
|
from .Constituenta import Constituenta, CstType, extract_entities, extract_globals
|
||||||
|
|
||||||
|
INSERT_LAST: int = -1
|
||||||
DELETED_ALIAS = 'DEL'
|
DELETED_ALIAS = 'DEL'
|
||||||
|
|
||||||
|
|
||||||
class RSForm:
|
class RSForm:
|
||||||
''' RSForm wrapper. No caching, each mutation requires querying. '''
|
''' RSForm wrapper. No caching, each mutation requires querying. '''
|
||||||
|
|
||||||
def __init__(self, model: LibraryItem) -> None:
|
def __init__(self, model: LibraryItem):
|
||||||
assert model.item_type == LibraryItemType.RSFORM
|
assert model.item_type == LibraryItemType.RSFORM
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
|
|
@ -271,25 +271,6 @@ class RSForm:
|
||||||
mapping[original.alias] = substitution.alias
|
mapping[original.alias] = substitution.alias
|
||||||
deleted.append(original.pk)
|
deleted.append(original.pk)
|
||||||
replacements.append(substitution.pk)
|
replacements.append(substitution.pk)
|
||||||
|
|
||||||
attributions = list(Attribution.objects.filter(container__schema=self.model))
|
|
||||||
if attributions:
|
|
||||||
orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions}
|
|
||||||
orig_pks = set(orig_to_sub.keys())
|
|
||||||
|
|
||||||
for attr in attributions:
|
|
||||||
if attr.container_id not in orig_pks and attr.attribute_id not in orig_pks:
|
|
||||||
continue
|
|
||||||
|
|
||||||
container_id = orig_to_sub.get(attr.container_id)
|
|
||||||
container_id = container_id if container_id is not None else attr.container_id
|
|
||||||
attr_id = orig_to_sub.get(attr.attribute_id)
|
|
||||||
attr_id = attr_id if attr_id is not None else attr.attribute_id
|
|
||||||
if not any(a.container_id == container_id and a.attribute_id == attr_id for a in attributions):
|
|
||||||
attr.attribute_id = attr_id
|
|
||||||
attr.container_id = container_id
|
|
||||||
attr.save()
|
|
||||||
|
|
||||||
Constituenta.objects.filter(pk__in=deleted).delete()
|
Constituenta.objects.filter(pk__in=deleted).delete()
|
||||||
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
cst_list = Constituenta.objects.filter(schema=self.model).only(
|
||||||
'alias', 'cst_type', 'definition_formal',
|
'alias', 'cst_type', 'definition_formal',
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,28 @@ from apps.library.models import LibraryItem, LibraryItemType
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
|
|
||||||
from .api_RSLanguage import generate_structure, get_type_prefix, guess_type
|
from .api_RSLanguage import generate_structure, get_type_prefix, guess_type
|
||||||
from .Attribution import Attribution
|
|
||||||
from .Constituenta import Constituenta, CstType
|
from .Constituenta import Constituenta, CstType
|
||||||
from .RSForm import DELETED_ALIAS, RSForm
|
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
||||||
|
|
||||||
|
|
||||||
class RSFormCached:
|
class RSFormCached:
|
||||||
''' RSForm cached. Caching allows to avoid querying for each method call. '''
|
''' RSForm cached. Caching allows to avoid querying for each method call. '''
|
||||||
|
|
||||||
def __init__(self, item_id: int) -> None:
|
def __init__(self, model: LibraryItem):
|
||||||
self.pk = item_id
|
self.model = model
|
||||||
self.cache: _RSFormCache = _RSFormCache(self)
|
self.cache: _RSFormCache = _RSFormCache(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(**kwargs) -> 'RSFormCached':
|
def create(**kwargs) -> 'RSFormCached':
|
||||||
''' Create LibraryItem via RSForm. '''
|
''' Create LibraryItem via RSForm. '''
|
||||||
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
|
model = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, **kwargs)
|
||||||
return RSFormCached(model.pk)
|
return RSFormCached(model)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_id(pk: int) -> 'RSFormCached':
|
||||||
|
''' Get LibraryItem by pk. '''
|
||||||
|
model = LibraryItem.objects.get(pk=pk)
|
||||||
|
return RSFormCached(model)
|
||||||
|
|
||||||
def get_dependant(self, target: Iterable[int]) -> set[int]:
|
def get_dependant(self, target: Iterable[int]) -> set[int]:
|
||||||
''' Get list of constituents depending on target (only 1st degree). '''
|
''' Get list of constituents depending on target (only 1st degree). '''
|
||||||
|
|
@ -45,7 +50,7 @@ class RSFormCached:
|
||||||
|
|
||||||
def constituentsQ(self) -> QuerySet[Constituenta]:
|
def constituentsQ(self) -> QuerySet[Constituenta]:
|
||||||
''' Get QuerySet containing all constituents of current RSForm. '''
|
''' Get QuerySet containing all constituents of current RSForm. '''
|
||||||
return Constituenta.objects.filter(schema_id=self.pk)
|
return Constituenta.objects.filter(schema=self.model)
|
||||||
|
|
||||||
def insert_last(
|
def insert_last(
|
||||||
self,
|
self,
|
||||||
|
|
@ -56,9 +61,9 @@ class RSFormCached:
|
||||||
''' Insert new constituenta at last position. '''
|
''' Insert new constituenta at last position. '''
|
||||||
if cst_type is None:
|
if cst_type is None:
|
||||||
cst_type = guess_type(alias)
|
cst_type = guess_type(alias)
|
||||||
position = Constituenta.objects.filter(schema_id=self.pk).count()
|
position = Constituenta.objects.filter(schema=self.model).count()
|
||||||
result = Constituenta.objects.create(
|
result = Constituenta.objects.create(
|
||||||
schema_id=self.pk,
|
schema=self.model,
|
||||||
order=position,
|
order=position,
|
||||||
alias=alias,
|
alias=alias,
|
||||||
cst_type=cst_type,
|
cst_type=cst_type,
|
||||||
|
|
@ -70,14 +75,14 @@ class RSFormCached:
|
||||||
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
def create_cst(self, data: dict, insert_after: Optional[Constituenta] = None) -> Constituenta:
|
||||||
''' Create constituenta from data. '''
|
''' Create constituenta from data. '''
|
||||||
self.cache.ensure_loaded_terms()
|
self.cache.ensure_loaded_terms()
|
||||||
if insert_after:
|
if insert_after is not None:
|
||||||
position = self.cache.by_id[insert_after.pk].order + 1
|
position = self.cache.by_id[insert_after.pk].order + 1
|
||||||
else:
|
else:
|
||||||
position = len(self.cache.constituents)
|
position = len(self.cache.constituents)
|
||||||
RSForm.shift_positions(position, 1, self.cache.constituents)
|
RSForm.shift_positions(position, 1, self.cache.constituents)
|
||||||
|
|
||||||
result = Constituenta.objects.create(
|
result = Constituenta.objects.create(
|
||||||
schema_id=self.pk,
|
schema=self.model,
|
||||||
order=position,
|
order=position,
|
||||||
alias=data['alias'],
|
alias=data['alias'],
|
||||||
cst_type=data['cst_type'],
|
cst_type=data['cst_type'],
|
||||||
|
|
@ -103,77 +108,49 @@ class RSFormCached:
|
||||||
RSForm.resolve_term_change(self.cache.constituents, [result.pk], self.cache.by_alias, self.cache.by_id)
|
RSForm.resolve_term_change(self.cache.constituents, [result.pk], self.cache.by_alias, self.cache.by_id)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def insert_from(
|
|
||||||
self, sourceID: int,
|
|
||||||
items_list: Optional[list[int]] = None,
|
|
||||||
initial_mapping: Optional[dict[str, str]] = None
|
|
||||||
) -> list[tuple[Constituenta, Constituenta]]:
|
|
||||||
''' Insert copy of constituents from source schema. '''
|
|
||||||
if not items_list:
|
|
||||||
items = list(Constituenta.objects.filter(schema_id=sourceID).order_by('order'))
|
|
||||||
else:
|
|
||||||
items = list(Constituenta.objects.filter(pk__in=items_list, schema_id=sourceID).order_by('order'))
|
|
||||||
if not items:
|
|
||||||
return []
|
|
||||||
new_constituents = self.insert_copy(items=items, initial_mapping=initial_mapping)
|
|
||||||
return list(zip(items, new_constituents))
|
|
||||||
|
|
||||||
def insert_copy(
|
def insert_copy(
|
||||||
self,
|
self,
|
||||||
items: list[Constituenta],
|
items: list[Constituenta],
|
||||||
position: Optional[int] = None,
|
position: int = INSERT_LAST,
|
||||||
initial_mapping: Optional[dict[str, str]] = None
|
initial_mapping: Optional[dict[str, str]] = None
|
||||||
) -> list[Constituenta]:
|
) -> list[Constituenta]:
|
||||||
''' Insert copy of target constituents updating references. '''
|
''' Insert copy of target constituents updating references. '''
|
||||||
if not items:
|
count = len(items)
|
||||||
|
if count == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
self.cache.ensure_loaded()
|
self.cache.ensure_loaded()
|
||||||
last_position = len(self.cache.constituents)
|
lastPosition = len(self.cache.constituents)
|
||||||
if not position:
|
if position == INSERT_LAST:
|
||||||
position = last_position
|
position = lastPosition
|
||||||
else:
|
else:
|
||||||
position = max(0, min(position, last_position))
|
position = max(0, min(position, lastPosition))
|
||||||
|
RSForm.shift_positions(position, count, self.cache.constituents)
|
||||||
|
|
||||||
was_empty = last_position == 0
|
indices: dict[str, int] = {}
|
||||||
if not was_empty and position != last_position:
|
for (value, _) in CstType.choices:
|
||||||
RSForm.shift_positions(position, len(items), self.cache.constituents)
|
indices[value] = -1
|
||||||
|
|
||||||
mapping_alias: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
|
mapping: dict[str, str] = initial_mapping.copy() if initial_mapping else {}
|
||||||
if not was_empty:
|
for cst in items:
|
||||||
indices: dict[str, int] = {}
|
if indices[cst.cst_type] == -1:
|
||||||
for (value, _) in CstType.choices:
|
indices[cst.cst_type] = self._get_max_index(cst.cst_type)
|
||||||
indices[value] = self._get_max_index(value)
|
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
||||||
|
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
||||||
|
mapping[cst.alias] = newAlias
|
||||||
|
|
||||||
for cst in items:
|
result = deepcopy(items)
|
||||||
indices[cst.cst_type] = indices[cst.cst_type] + 1
|
for cst in result:
|
||||||
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'
|
|
||||||
mapping_alias[cst.alias] = newAlias
|
|
||||||
|
|
||||||
source_ids = [cst.id for cst in items]
|
|
||||||
new_constituents = deepcopy(items)
|
|
||||||
for cst in new_constituents:
|
|
||||||
cst.pk = None
|
cst.pk = None
|
||||||
cst.schema_id = self.pk
|
cst.schema = self.model
|
||||||
cst.order = position
|
cst.order = position
|
||||||
if mapping_alias:
|
cst.alias = mapping[cst.alias]
|
||||||
cst.alias = mapping_alias[cst.alias]
|
cst.apply_mapping(mapping)
|
||||||
cst.apply_mapping(mapping_alias)
|
|
||||||
position = position + 1
|
position = position + 1
|
||||||
|
|
||||||
new_constituents = Constituenta.objects.bulk_create(new_constituents)
|
new_cst = Constituenta.objects.bulk_create(result)
|
||||||
|
self.cache.insert_multi(new_cst)
|
||||||
mapping_id: dict[int, int] = {source_ids[i]: new_constituents[i].id for i in range(len(source_ids))}
|
return result
|
||||||
attributions = list(Attribution.objects.filter(container__in=source_ids, attribute__in=source_ids))
|
|
||||||
for attr in attributions:
|
|
||||||
attr.pk = None
|
|
||||||
attr.container_id = mapping_id[attr.container_id]
|
|
||||||
attr.attribute_id = mapping_id[attr.attribute_id]
|
|
||||||
|
|
||||||
Attribution.objects.bulk_create(attributions)
|
|
||||||
|
|
||||||
self.cache.insert_multi(new_constituents)
|
|
||||||
return new_constituents
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
def update_cst(self, target: int, data: dict) -> dict:
|
def update_cst(self, target: int, data: dict) -> dict:
|
||||||
|
|
@ -256,26 +233,6 @@ class RSFormCached:
|
||||||
mapping[original.alias] = substitution.alias
|
mapping[original.alias] = substitution.alias
|
||||||
deleted.append(original)
|
deleted.append(original)
|
||||||
replacements.append(substitution.pk)
|
replacements.append(substitution.pk)
|
||||||
|
|
||||||
attributions = list(Attribution.objects.filter(container__schema_id=self.pk))
|
|
||||||
if attributions:
|
|
||||||
orig_to_sub = {original.pk: substitution.pk for original, substitution in substitutions}
|
|
||||||
orig_pks = set(orig_to_sub.keys())
|
|
||||||
|
|
||||||
for attr in attributions:
|
|
||||||
if attr.container_id not in orig_pks and attr.attribute_id not in orig_pks:
|
|
||||||
continue
|
|
||||||
|
|
||||||
container_id = orig_to_sub.get(attr.container_id)
|
|
||||||
container_id = container_id if container_id is not None else attr.container_id
|
|
||||||
attr_id = orig_to_sub.get(attr.attribute_id)
|
|
||||||
attr_id = attr_id if attr_id is not None else attr.attribute_id
|
|
||||||
if attr_id != container_id and not any(
|
|
||||||
a.container_id == container_id and a.attribute_id == attr_id for a in attributions):
|
|
||||||
attr.attribute_id = attr_id
|
|
||||||
attr.container_id = container_id
|
|
||||||
attr.save()
|
|
||||||
|
|
||||||
self.cache.remove_multi(deleted)
|
self.cache.remove_multi(deleted)
|
||||||
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
|
Constituenta.objects.filter(pk__in=[cst.pk for cst in deleted]).delete()
|
||||||
RSForm.save_order(self.cache.constituents)
|
RSForm.save_order(self.cache.constituents)
|
||||||
|
|
@ -368,7 +325,7 @@ class RSFormCached:
|
||||||
prefix = get_type_prefix(cst_type)
|
prefix = get_type_prefix(cst_type)
|
||||||
for text in expressions:
|
for text in expressions:
|
||||||
new_item = Constituenta.objects.create(
|
new_item = Constituenta.objects.create(
|
||||||
schema_id=self.pk,
|
schema=self.model,
|
||||||
order=position,
|
order=position,
|
||||||
alias=f'{prefix}{free_index}',
|
alias=f'{prefix}{free_index}',
|
||||||
definition_formal=text,
|
definition_formal=text,
|
||||||
|
|
@ -386,7 +343,7 @@ class RSFormCached:
|
||||||
cst_list: Iterable[Constituenta] = []
|
cst_list: Iterable[Constituenta] = []
|
||||||
if not self.cache.is_loaded:
|
if not self.cache.is_loaded:
|
||||||
cst_list = Constituenta.objects \
|
cst_list = Constituenta.objects \
|
||||||
.filter(schema_id=self.pk, cst_type=cst_type) \
|
.filter(schema=self.model, cst_type=cst_type) \
|
||||||
.only('alias')
|
.only('alias')
|
||||||
else:
|
else:
|
||||||
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
|
cst_list = [cst for cst in self.cache.constituents if cst.cst_type == cst_type]
|
||||||
|
|
@ -400,7 +357,7 @@ class RSFormCached:
|
||||||
class _RSFormCache:
|
class _RSFormCache:
|
||||||
''' Cache for RSForm constituents. '''
|
''' Cache for RSForm constituents. '''
|
||||||
|
|
||||||
def __init__(self, schema: 'RSFormCached') -> None:
|
def __init__(self, schema: 'RSFormCached'):
|
||||||
self._schema = schema
|
self._schema = schema
|
||||||
self.constituents: list[Constituenta] = []
|
self.constituents: list[Constituenta] = []
|
||||||
self.by_id: dict[int, Constituenta] = {}
|
self.by_id: dict[int, Constituenta] = {}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from .RSFormCached import RSFormCached
|
||||||
class SemanticInfo:
|
class SemanticInfo:
|
||||||
''' Semantic information derived from constituents. '''
|
''' Semantic information derived from constituents. '''
|
||||||
|
|
||||||
def __init__(self, schema: RSFormCached) -> None:
|
def __init__(self, schema: RSFormCached):
|
||||||
schema.cache.ensure_loaded()
|
schema.cache.ensure_loaded()
|
||||||
self._items = schema.cache.constituents
|
self._items = schema.cache.constituents
|
||||||
self._cst_by_ID = schema.cache.by_id
|
self._cst_by_ID = schema.cache.by_id
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@
|
||||||
from .Attribution import Attribution
|
from .Attribution import Attribution
|
||||||
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
from .Constituenta import Constituenta, CstType, extract_globals, replace_entities, replace_globals
|
||||||
from .OrderManager import OrderManager
|
from .OrderManager import OrderManager
|
||||||
from .RSForm import DELETED_ALIAS, RSForm
|
from .RSForm import DELETED_ALIAS, INSERT_LAST, RSForm
|
||||||
from .RSFormCached import RSFormCached
|
from .RSFormCached import RSFormCached
|
||||||
|
|
|
||||||
|
|
@ -228,10 +228,10 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
'id': oss.pk,
|
'id': oss.pk,
|
||||||
'alias': oss.alias
|
'alias': oss.alias
|
||||||
})
|
})
|
||||||
for attrib in Attribution.objects.filter(container__schema=instance).only('container_id', 'attribute_id'):
|
for assoc in Attribution.objects.filter(container__schema=instance).only('container_id', 'attribute_id'):
|
||||||
result['attribution'].append({
|
result['attribution'].append({
|
||||||
'container': attrib.container_id,
|
'container': assoc.container_id,
|
||||||
'attribute': attrib.attribute_id
|
'attribute': assoc.attribute_id
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
@ -304,9 +304,9 @@ class RSFormSerializer(StrictModelSerializer):
|
||||||
|
|
||||||
Attribution.objects.filter(container__schema=instance).delete()
|
Attribution.objects.filter(container__schema=instance).delete()
|
||||||
attributions_to_create: list[Attribution] = []
|
attributions_to_create: list[Attribution] = []
|
||||||
for attrib in data.get('attribution', []):
|
for assoc in data.get('attribution', []):
|
||||||
old_container_id = attrib['container']
|
old_container_id = assoc['container']
|
||||||
old_attribute_id = attrib['attribute']
|
old_attribute_id = assoc['attribute']
|
||||||
container_id = id_map.get(old_container_id)
|
container_id = id_map.get(old_container_id)
|
||||||
attribute_id = id_map.get(old_attribute_id)
|
attribute_id = id_map.get(old_attribute_id)
|
||||||
if container_id and attribute_id:
|
if container_id and attribute_id:
|
||||||
|
|
@ -436,7 +436,7 @@ class InlineSynthesisSerializer(StrictSerializer):
|
||||||
''' Serializer: Inline synthesis operation input. '''
|
''' Serializer: Inline synthesis operation input. '''
|
||||||
receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id'))
|
receiver = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id'))
|
||||||
source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore
|
source = PKField(many=False, queryset=LibraryItem.objects.all().only('owner_id')) # type: ignore
|
||||||
items = PKField(many=True, queryset=Constituenta.objects.all().only('schema_id'))
|
items = PKField(many=True, queryset=Constituenta.objects.all())
|
||||||
substitutions = serializers.ListField(
|
substitutions = serializers.ListField(
|
||||||
child=SubstitutionSerializerBase()
|
child=SubstitutionSerializerBase()
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
result['description'] = data.get('description', '')
|
result['description'] = data.get('description', '')
|
||||||
if 'id' in data:
|
if 'id' in data:
|
||||||
result['id'] = data['id']
|
result['id'] = data['id']
|
||||||
self.instance = RSFormCached(result['id'])
|
self.instance = RSFormCached.from_id(result['id'])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def validate(self, attrs: dict):
|
def validate(self, attrs: dict):
|
||||||
|
|
@ -151,7 +151,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
for cst_data in validated_data['items']:
|
for cst_data in validated_data['items']:
|
||||||
cst = Constituenta(
|
cst = Constituenta(
|
||||||
alias=cst_data['alias'],
|
alias=cst_data['alias'],
|
||||||
schema_id=self.instance.pk,
|
schema=self.instance.model,
|
||||||
order=order,
|
order=order,
|
||||||
cst_type=cst_data['cstType'],
|
cst_type=cst_data['cstType'],
|
||||||
)
|
)
|
||||||
|
|
@ -163,13 +163,12 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def update(self, instance: RSFormCached, validated_data) -> RSFormCached:
|
def update(self, instance: RSFormCached, validated_data) -> RSFormCached:
|
||||||
model = LibraryItem.objects.get(pk=instance.pk)
|
|
||||||
if 'alias' in validated_data:
|
if 'alias' in validated_data:
|
||||||
model.alias = validated_data['alias']
|
instance.model.alias = validated_data['alias']
|
||||||
if 'title' in validated_data:
|
if 'title' in validated_data:
|
||||||
model.title = validated_data['title']
|
instance.model.title = validated_data['title']
|
||||||
if 'description' in validated_data:
|
if 'description' in validated_data:
|
||||||
model.description = validated_data['description']
|
instance.model.description = validated_data['description']
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
prev_constituents = instance.constituentsQ()
|
prev_constituents = instance.constituentsQ()
|
||||||
|
|
@ -186,7 +185,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
else:
|
else:
|
||||||
cst = Constituenta(
|
cst = Constituenta(
|
||||||
alias=cst_data['alias'],
|
alias=cst_data['alias'],
|
||||||
schema_id=instance.pk,
|
schema=instance.model,
|
||||||
order=order,
|
order=order,
|
||||||
cst_type=cst_data['cstType'],
|
cst_type=cst_data['cstType'],
|
||||||
)
|
)
|
||||||
|
|
@ -200,7 +199,7 @@ class RSFormTRSSerializer(serializers.Serializer):
|
||||||
prev_cst.delete()
|
prev_cst.delete()
|
||||||
|
|
||||||
instance.resolve_all_text()
|
instance.resolve_all_text()
|
||||||
model.save()
|
instance.model.save()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from ..models import Constituenta, CstType
|
||||||
class PyConceptAdapter:
|
class PyConceptAdapter:
|
||||||
''' RSForm adapter for interacting with pyconcept module. '''
|
''' RSForm adapter for interacting with pyconcept module. '''
|
||||||
|
|
||||||
def __init__(self, data: Union[int, dict]) -> None:
|
def __init__(self, data: Union[int, dict]):
|
||||||
try:
|
try:
|
||||||
if 'items' in cast(dict, data):
|
if 'items' in cast(dict, data):
|
||||||
self.data = self._prepare_request_raw(cast(dict, data))
|
self.data = self._prepare_request_raw(cast(dict, data))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
''' Testing models: RSForm. '''
|
''' Testing models: api_RSForm. '''
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
|
|
||||||
from apps.rsform.models import Constituenta, CstType, RSForm
|
from apps.rsform.models import Constituenta, CstType, RSForm
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
''' Testing models: RSFormCached. '''
|
''' Testing models: api_RSForm. '''
|
||||||
from apps.rsform.models import Attribution, Constituenta, CstType, OrderManager, RSFormCached
|
from django.forms import ValidationError
|
||||||
|
|
||||||
|
from apps.rsform.models import Constituenta, CstType, OrderManager, RSFormCached
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared.DBTester import DBTester
|
from shared.DBTester import DBTester
|
||||||
|
|
||||||
|
|
@ -22,8 +24,8 @@ class TestRSFormCached(DBTester):
|
||||||
self.assertFalse(schema1.constituentsQ().exists())
|
self.assertFalse(schema1.constituentsQ().exists())
|
||||||
self.assertFalse(schema2.constituentsQ().exists())
|
self.assertFalse(schema2.constituentsQ().exists())
|
||||||
|
|
||||||
Constituenta.objects.create(alias='X1', schema_id=schema1.pk, order=0)
|
Constituenta.objects.create(alias='X1', schema=schema1.model, order=0)
|
||||||
Constituenta.objects.create(alias='X2', schema_id=schema1.pk, order=1)
|
Constituenta.objects.create(alias='X2', schema=schema1.model, order=1)
|
||||||
self.assertTrue(schema1.constituentsQ().exists())
|
self.assertTrue(schema1.constituentsQ().exists())
|
||||||
self.assertFalse(schema2.constituentsQ().exists())
|
self.assertFalse(schema2.constituentsQ().exists())
|
||||||
self.assertEqual(schema1.constituentsQ().count(), 2)
|
self.assertEqual(schema1.constituentsQ().count(), 2)
|
||||||
|
|
@ -32,7 +34,7 @@ class TestRSFormCached(DBTester):
|
||||||
def test_insert_last(self):
|
def test_insert_last(self):
|
||||||
x1 = self.schema.insert_last('X1')
|
x1 = self.schema.insert_last('X1')
|
||||||
self.assertEqual(x1.order, 0)
|
self.assertEqual(x1.order, 0)
|
||||||
self.assertEqual(x1.schema_id, self.schema.pk)
|
self.assertEqual(x1.schema, self.schema.model)
|
||||||
|
|
||||||
|
|
||||||
def test_create_cst(self):
|
def test_create_cst(self):
|
||||||
|
|
@ -106,25 +108,6 @@ class TestRSFormCached(DBTester):
|
||||||
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
self.assertEqual(s2.definition_raw, '@{X11|plur}')
|
||||||
|
|
||||||
|
|
||||||
def test_insert_from(self):
|
|
||||||
self.schema.insert_last('X2')
|
|
||||||
self.schema.insert_last('D2')
|
|
||||||
self.schema.insert_last('X3')
|
|
||||||
self.schema.insert_last(
|
|
||||||
alias='D1',
|
|
||||||
definition_formal='X2 = X3'
|
|
||||||
)
|
|
||||||
test_ks = RSFormCached.create(title='Test')
|
|
||||||
test_ks.insert_from(self.schema.pk)
|
|
||||||
items = Constituenta.objects.filter(schema_id=test_ks.pk).order_by('order')
|
|
||||||
self.assertEqual(len(items), 4)
|
|
||||||
self.assertEqual(items[0].alias, 'X2')
|
|
||||||
self.assertEqual(items[1].alias, 'D2')
|
|
||||||
self.assertEqual(items[2].alias, 'X3')
|
|
||||||
self.assertEqual(items[3].alias, 'D1')
|
|
||||||
self.assertEqual(items[3].definition_formal, 'X2 = X3')
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_cst(self):
|
def test_delete_cst(self):
|
||||||
x1 = self.schema.insert_last('X1')
|
x1 = self.schema.insert_last('X1')
|
||||||
x2 = self.schema.insert_last('X2')
|
x2 = self.schema.insert_last('X2')
|
||||||
|
|
@ -187,24 +170,6 @@ class TestRSFormCached(DBTester):
|
||||||
self.assertEqual(d1.definition_formal, x2.alias)
|
self.assertEqual(d1.definition_formal, x2.alias)
|
||||||
|
|
||||||
|
|
||||||
def test_substitute_attributions(self):
|
|
||||||
x1 = self.schema.insert_last(alias='X1')
|
|
||||||
x2 = self.schema.insert_last(alias='X2')
|
|
||||||
d1 = self.schema.insert_last(alias='D1')
|
|
||||||
d2 = self.schema.insert_last(alias='D2')
|
|
||||||
|
|
||||||
Attribution.objects.create(container=x1, attribute=d2)
|
|
||||||
Attribution.objects.create(container=x2, attribute=d2)
|
|
||||||
Attribution.objects.create(container=x1, attribute=x2)
|
|
||||||
Attribution.objects.create(container=x1, attribute=d1)
|
|
||||||
|
|
||||||
self.schema.substitute([(x1, x2)])
|
|
||||||
self.assertEqual(self.schema.constituentsQ().count(), 3)
|
|
||||||
self.assertEqual(Attribution.objects.filter(container__schema_id=self.schema.pk).count(), 2)
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x2, attribute=d2).exists())
|
|
||||||
self.assertTrue(Attribution.objects.filter(container=x2, attribute=d1).exists())
|
|
||||||
|
|
||||||
|
|
||||||
def test_restore_order(self):
|
def test_restore_order(self):
|
||||||
d2 = self.schema.insert_last(
|
d2 = self.schema.insert_last(
|
||||||
alias='D2',
|
alias='D2',
|
||||||
|
|
@ -229,10 +194,6 @@ class TestRSFormCached(DBTester):
|
||||||
alias='A1',
|
alias='A1',
|
||||||
definition_formal=r'D3=∅',
|
definition_formal=r'D3=∅',
|
||||||
)
|
)
|
||||||
a2 = self.schema.insert_last(
|
|
||||||
alias='A2',
|
|
||||||
definition_formal=r'P1[S1]',
|
|
||||||
)
|
|
||||||
d3 = self.schema.insert_last(
|
d3 = self.schema.insert_last(
|
||||||
alias='D3',
|
alias='D3',
|
||||||
definition_formal=r'Pr2(S2)',
|
definition_formal=r'Pr2(S2)',
|
||||||
|
|
@ -249,10 +210,6 @@ class TestRSFormCached(DBTester):
|
||||||
alias='F2',
|
alias='F2',
|
||||||
definition_formal=r'[α∈ℬ(X1)] X1\α',
|
definition_formal=r'[α∈ℬ(X1)] X1\α',
|
||||||
)
|
)
|
||||||
p1 = self.schema.insert_last(
|
|
||||||
alias='P1',
|
|
||||||
definition_formal=r'[α∈ℬ(R1)] card(α)=0',
|
|
||||||
)
|
|
||||||
|
|
||||||
OrderManager(self.schema).restore_order()
|
OrderManager(self.schema).restore_order()
|
||||||
x1.refresh_from_db()
|
x1.refresh_from_db()
|
||||||
|
|
@ -267,23 +224,19 @@ class TestRSFormCached(DBTester):
|
||||||
f1.refresh_from_db()
|
f1.refresh_from_db()
|
||||||
f2.refresh_from_db()
|
f2.refresh_from_db()
|
||||||
a1.refresh_from_db()
|
a1.refresh_from_db()
|
||||||
a2.refresh_from_db()
|
|
||||||
p1.refresh_from_db()
|
|
||||||
|
|
||||||
self.assertEqual(x1.order, 0)
|
self.assertEqual(x1.order, 0)
|
||||||
self.assertEqual(x2.order, 1)
|
self.assertEqual(x2.order, 1)
|
||||||
self.assertEqual(c1.order, 2)
|
self.assertEqual(c1.order, 2)
|
||||||
self.assertEqual(p1.order, 3)
|
self.assertEqual(s1.order, 3)
|
||||||
self.assertEqual(s1.order, 4)
|
self.assertEqual(d1.order, 4)
|
||||||
self.assertEqual(a2.order, 5)
|
self.assertEqual(s2.order, 5)
|
||||||
self.assertEqual(d1.order, 6)
|
self.assertEqual(d3.order, 6)
|
||||||
self.assertEqual(s2.order, 7)
|
self.assertEqual(a1.order, 7)
|
||||||
self.assertEqual(d3.order, 8)
|
self.assertEqual(d4.order, 8)
|
||||||
self.assertEqual(a1.order, 9)
|
self.assertEqual(d2.order, 9)
|
||||||
self.assertEqual(d4.order, 10)
|
self.assertEqual(f1.order, 10)
|
||||||
self.assertEqual(d2.order, 11)
|
self.assertEqual(f2.order, 11)
|
||||||
self.assertEqual(f1.order, 12)
|
|
||||||
self.assertEqual(f2.order, 13)
|
|
||||||
|
|
||||||
|
|
||||||
def test_reset_aliases(self):
|
def test_reset_aliases(self):
|
||||||
|
|
|
||||||
|
|
@ -42,13 +42,13 @@ class TestAttributionsEndpoints(EndpointTester):
|
||||||
self.executeBadData(data, item=self.owned_id)
|
self.executeBadData(data, item=self.owned_id)
|
||||||
|
|
||||||
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
|
data = {'container': self.n1.pk, 'attribute': self.x1.pk}
|
||||||
self.executeForbidden(data, item=self.unowned_id)
|
self.executeBadData(data, item=self.unowned_id)
|
||||||
|
|
||||||
response = self.executeCreated(data, item=self.owned_id)
|
response = self.executeCreated(data, item=self.owned_id)
|
||||||
attributions = response.data['attribution']
|
associations = response.data['attribution']
|
||||||
self.assertEqual(len(attributions), 1)
|
self.assertEqual(len(associations), 1)
|
||||||
self.assertEqual(attributions[0]['container'], self.n1.pk)
|
self.assertEqual(associations[0]['container'], self.n1.pk)
|
||||||
self.assertEqual(attributions[0]['attribute'], self.x1.pk)
|
self.assertEqual(associations[0]['attribute'], self.x1.pk)
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
@decl_endpoint('/api/rsforms/{item}/create-attribution', method='post')
|
||||||
|
|
@ -94,7 +94,7 @@ class TestAttributionsEndpoints(EndpointTester):
|
||||||
attribute=self.n1
|
attribute=self.n1
|
||||||
)
|
)
|
||||||
response = self.executeOK(data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
attributions = response.data['attribution']
|
associations = response.data['attribution']
|
||||||
self.assertEqual(len(attributions), 1)
|
self.assertEqual(len(associations), 1)
|
||||||
self.assertEqual(attributions[0]['container'], self.n2.pk)
|
self.assertEqual(associations[0]['container'], self.n2.pk)
|
||||||
self.assertEqual(attributions[0]['attribute'], self.n1.pk)
|
self.assertEqual(associations[0]['attribute'], self.n1.pk)
|
||||||
|
|
|
||||||
|
|
@ -238,55 +238,11 @@ class TestRSFormViewset(EndpointTester):
|
||||||
'substitution': d2.pk
|
'substitution': d2.pk
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
self.executeOK(data, item=self.owned_id)
|
response = self.executeOK(data, item=self.owned_id)
|
||||||
d3.refresh_from_db()
|
d3.refresh_from_db()
|
||||||
self.assertEqual(d3.definition_formal, r'D1 \ D2')
|
self.assertEqual(d3.definition_formal, r'D1 \ D2')
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/substitute', method='patch')
|
|
||||||
def test_substitute_with_attributions(self):
|
|
||||||
self.set_params(item=self.owned_id)
|
|
||||||
|
|
||||||
# Create two base items
|
|
||||||
x1 = self.owned.insert_last('X1')
|
|
||||||
x2 = self.owned.insert_last('X2')
|
|
||||||
|
|
||||||
# Create two attributes to be attributions
|
|
||||||
a1 = self.owned.insert_last('A1', cst_type=CstType.BASE)
|
|
||||||
a2 = self.owned.insert_last('A2', cst_type=CstType.BASE)
|
|
||||||
|
|
||||||
# Create attributions: X1 -> A1, X2 -> A2
|
|
||||||
Attribution = self.owned.constituentsQ().model._meta.apps.get_model('rsform', 'Attribution')
|
|
||||||
Attribution.objects.create(container=x1, attribute=a1)
|
|
||||||
Attribution.objects.create(container=x2, attribute=a2)
|
|
||||||
|
|
||||||
# Substitute x1 with x2
|
|
||||||
data = {
|
|
||||||
'substitutions': [{
|
|
||||||
'original': x1.pk,
|
|
||||||
'substitution': x2.pk
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
self.executeOK(data, item=self.owned_id)
|
|
||||||
|
|
||||||
# Fetch updated attributions
|
|
||||||
attributions = Attribution.objects.filter(
|
|
||||||
container__in=[x1.pk, x2.pk],
|
|
||||||
attribute__in=[a1.pk, a2.pk]
|
|
||||||
)
|
|
||||||
self.assertEqual(len(attributions), 2)
|
|
||||||
|
|
||||||
# Confirm the attribution with container originally x1 is now x2, and there are no duplicates
|
|
||||||
containers = set()
|
|
||||||
attributes = set()
|
|
||||||
for attr in attributions:
|
|
||||||
containers.add(attr.container_id)
|
|
||||||
attributes.add(attr.attribute_id)
|
|
||||||
self.assertIn(x2.pk, containers)
|
|
||||||
self.assertIn(a1.pk, attributes)
|
|
||||||
self.assertIn(a2.pk, attributes)
|
|
||||||
|
|
||||||
|
|
||||||
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
@decl_endpoint('/api/rsforms/{item}/create-cst', method='post')
|
||||||
def test_create_constituenta_data(self):
|
def test_create_constituenta_data(self):
|
||||||
data = {
|
data = {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead
|
||||||
from apps.library.serializers import LibraryItemSerializer
|
from apps.library.serializers import LibraryItemSerializer
|
||||||
from apps.oss.models import Inheritance, PropagationFacade
|
from apps.oss.models import PropagationFacade
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from shared import messages as msg
|
from shared import messages as msg
|
||||||
from shared import permissions, utility
|
from shared import permissions, utility
|
||||||
|
|
@ -49,7 +49,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
'restore_order',
|
'restore_order',
|
||||||
'reset_aliases',
|
'reset_aliases',
|
||||||
'produce_structure',
|
'produce_structure',
|
||||||
'create_attribution',
|
'add_attribution',
|
||||||
'delete_attribution',
|
'delete_attribution',
|
||||||
'clear_attributions'
|
'clear_attributions'
|
||||||
]:
|
]:
|
||||||
|
|
@ -91,10 +91,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
insert_after = data['insert_after']
|
insert_after = data['insert_after']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = PropagationFacade()
|
schema = m.RSFormCached(item)
|
||||||
schema = propagation.get_schema(item.pk)
|
|
||||||
new_cst = schema.create_cst(data, insert_after)
|
new_cst = schema.create_cst(data, insert_after)
|
||||||
propagation.after_create_cst([new_cst])
|
PropagationFacade.after_create_cst(schema, [new_cst])
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -126,10 +125,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
data = serializer.validated_data['item_data']
|
data = serializer.validated_data['item_data']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = PropagationFacade()
|
schema = m.RSFormCached(item)
|
||||||
schema = propagation.get_schema(item.pk)
|
|
||||||
old_data = schema.update_cst(cst.pk, data)
|
old_data = schema.update_cst(cst.pk, data)
|
||||||
propagation.after_update_cst(item.pk, cst.pk, data, old_data)
|
PropagationFacade.after_update_cst(schema, cst.pk, data, old_data)
|
||||||
if 'alias' in data and data['alias'] != cst.alias:
|
if 'alias' in data and data['alias'] != cst.alias:
|
||||||
cst.refresh_from_db()
|
cst.refresh_from_db()
|
||||||
changed_type = 'cst_type' in data and cst.cst_type != data['cst_type']
|
changed_type = 'cst_type' in data and cst.cst_type != data['cst_type']
|
||||||
|
|
@ -140,7 +138,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
cst.save()
|
cst.save()
|
||||||
schema.apply_mapping(mapping=mapping, change_aliases=False)
|
schema.apply_mapping(mapping=mapping, change_aliases=False)
|
||||||
if changed_type:
|
if changed_type:
|
||||||
propagation.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type))
|
PropagationFacade.after_change_cst_type(item.pk, cst.pk, cast(m.CstType, cst.cst_type))
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
|
|
@ -210,10 +208,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = PropagationFacade()
|
schema = m.RSFormCached(item)
|
||||||
schema = propagation.get_schema(item.pk)
|
|
||||||
new_cst = schema.produce_structure(cst, cst_parse)
|
new_cst = schema.produce_structure(cst, cst_parse)
|
||||||
propagation.after_create_cst(new_cst)
|
PropagationFacade.after_create_cst(schema, new_cst)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
|
|
@ -248,7 +245,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
original = cast(m.Constituenta, substitution['original'])
|
original = cast(m.Constituenta, substitution['original'])
|
||||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||||
substitutions.append((original, replacement))
|
substitutions.append((original, replacement))
|
||||||
PropagationFacade().before_substitute(item.pk, substitutions)
|
PropagationFacade.before_substitute(item.pk, substitutions)
|
||||||
schema.substitute(substitutions)
|
schema.substitute(substitutions)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
|
@ -278,7 +275,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
schema = m.RSForm(item)
|
schema = m.RSForm(item)
|
||||||
PropagationFacade().before_delete_cst(item.pk, [cst.pk for cst in cst_list])
|
PropagationFacade.before_delete_cst(item.pk, [cst.pk for cst in cst_list])
|
||||||
schema.delete_cst(cst_list)
|
schema.delete_cst(cst_list)
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
|
|
@ -308,16 +305,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
attribute = serializer.validated_data['attribute']
|
attribute = serializer.validated_data['attribute']
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if Inheritance.check_share_origin(container.pk, attribute.pk):
|
new_association = m.Attribution.objects.create(
|
||||||
raise ValidationError({
|
|
||||||
'container': msg.deleteInheritedAttribution()
|
|
||||||
})
|
|
||||||
|
|
||||||
new_attribution = m.Attribution.objects.create(
|
|
||||||
container=container,
|
container=container,
|
||||||
attribute=attribute
|
attribute=attribute
|
||||||
)
|
)
|
||||||
PropagationFacade().after_create_attribution(item.pk, [new_attribution])
|
PropagationFacade.after_create_attribution(item.pk, [new_association])
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -326,7 +318,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary='delete Attribution',
|
summary='delete Association',
|
||||||
tags=['RSForm'],
|
tags=['RSForm'],
|
||||||
request=s.AttributionDataSerializer,
|
request=s.AttributionDataSerializer,
|
||||||
responses={
|
responses={
|
||||||
|
|
@ -344,22 +336,17 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
target_query = m.Attribution.objects.filter(
|
target = list(m.Attribution.objects.filter(
|
||||||
container=serializer.validated_data['container'],
|
container=serializer.validated_data['container'],
|
||||||
attribute=serializer.validated_data['attribute']
|
attribute=serializer.validated_data['attribute']
|
||||||
)
|
))
|
||||||
attr = target_query.first()
|
if not target:
|
||||||
if not attr:
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'container': msg.missingAttribution()
|
'container': msg.invalidAssociation()
|
||||||
})
|
|
||||||
if Inheritance.check_share_origin(request.data['container'], request.data['attribute']):
|
|
||||||
raise ValidationError({
|
|
||||||
'container': msg.deleteInheritedAttribution()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
PropagationFacade().before_delete_attribution(item.pk, [attr])
|
PropagationFacade.before_delete_attribution(item.pk, target)
|
||||||
attr.delete()
|
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -386,14 +373,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
attributions = list(m.Attribution.objects.filter(container=serializer.validated_data['target']))
|
target = list(m.Attribution.objects.filter(container=serializer.validated_data['target']))
|
||||||
to_delete: list[m.Attribution] = []
|
if target:
|
||||||
for attrib in attributions:
|
PropagationFacade.before_delete_attribution(item.pk, target)
|
||||||
if not Inheritance.check_share_origin(attrib.container.pk, attrib.attribute.pk):
|
m.Attribution.objects.filter(pk__in=[assoc.pk for assoc in target]).delete()
|
||||||
to_delete.append(attrib)
|
|
||||||
if to_delete:
|
|
||||||
PropagationFacade().before_delete_attribution(item.pk, to_delete)
|
|
||||||
m.Attribution.objects.filter(pk__in=[attrib.pk for attrib in to_delete]).delete()
|
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -473,7 +456,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
item = self._get_item()
|
item = self._get_item()
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
m.OrderManager(m.RSFormCached(item.pk)).restore_order()
|
m.OrderManager(m.RSFormCached(item)).restore_order()
|
||||||
item.save(update_fields=['time_update'])
|
item.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -510,10 +493,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
|
||||||
|
|
||||||
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)
|
serializer.is_valid(raise_exception=True)
|
||||||
result: m.RSFormCached = serializer.save()
|
result: m.RSForm = serializer.save()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(LibraryItem.objects.get(pk=result.pk)).data
|
data=s.RSFormParseSerializer(result.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
|
@ -668,10 +651,10 @@ class TrsImportView(views.APIView):
|
||||||
_prepare_rsform_data(data, request, owner)
|
_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)
|
serializer.is_valid(raise_exception=True)
|
||||||
schema: m.RSFormCached = serializer.save()
|
schema: m.RSForm = serializer.save()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data
|
data=LibraryItemSerializer(schema.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -704,10 +687,10 @@ def create_rsform(request: Request) -> HttpResponse:
|
||||||
_prepare_rsform_data(data, request, owner)
|
_prepare_rsform_data(data, request, owner)
|
||||||
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
serializer_rsform = s.RSFormTRSSerializer(data=data, context={'load_meta': True})
|
||||||
serializer_rsform.is_valid(raise_exception=True)
|
serializer_rsform.is_valid(raise_exception=True)
|
||||||
schema: m.RSFormCached = serializer_rsform.save()
|
schema: m.RSForm = serializer_rsform.save()
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_201_CREATED,
|
status=c.HTTP_201_CREATED,
|
||||||
data=LibraryItemSerializer(LibraryItem.objects.get(pk=schema.pk)).data
|
data=LibraryItemSerializer(schema.model).data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -748,34 +731,33 @@ def inline_synthesis(request: Request) -> HttpResponse:
|
||||||
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)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = cast(LibraryItem, serializer.validated_data['receiver'])
|
receiver = m.RSFormCached(serializer.validated_data['receiver'])
|
||||||
target_cst = cast(list[m.Constituenta], serializer.validated_data['items'])
|
items = cast(list[m.Constituenta], serializer.validated_data['items'])
|
||||||
source = cast(LibraryItem, serializer.validated_data['source'])
|
if not items:
|
||||||
target_ids = [item.pk for item in target_cst] if target_cst else None
|
source = cast(LibraryItem, serializer.validated_data['source'])
|
||||||
|
items = list(m.Constituenta.objects.filter(schema=source).order_by('order'))
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
propagation = PropagationFacade()
|
new_items = receiver.insert_copy(items)
|
||||||
receiver = propagation.get_schema(item.pk)
|
PropagationFacade.after_create_cst(receiver, new_items)
|
||||||
new_items = receiver.insert_from(source.pk, target_ids)
|
|
||||||
target_ids = [item[0].pk for item in new_items]
|
|
||||||
mapping_ids = {cst.pk: new_cst for (cst, new_cst) in new_items}
|
|
||||||
propagation.after_create_cst([item[1] for item in new_items])
|
|
||||||
|
|
||||||
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
substitutions: list[tuple[m.Constituenta, m.Constituenta]] = []
|
||||||
for substitution in serializer.validated_data['substitutions']:
|
for substitution in serializer.validated_data['substitutions']:
|
||||||
original = cast(m.Constituenta, substitution['original'])
|
original = cast(m.Constituenta, substitution['original'])
|
||||||
replacement = cast(m.Constituenta, substitution['substitution'])
|
replacement = cast(m.Constituenta, substitution['substitution'])
|
||||||
if original.pk in target_ids:
|
if original in items:
|
||||||
original = mapping_ids[original.pk]
|
index = next(i for (i, cst) in enumerate(items) if cst.pk == original.pk)
|
||||||
|
original = new_items[index]
|
||||||
else:
|
else:
|
||||||
replacement = mapping_ids[replacement.pk]
|
index = next(i for (i, cst) in enumerate(items) if cst.pk == replacement.pk)
|
||||||
|
replacement = new_items[index]
|
||||||
substitutions.append((original, replacement))
|
substitutions.append((original, replacement))
|
||||||
|
|
||||||
propagation.before_substitute(receiver.pk, substitutions)
|
PropagationFacade.before_substitute(receiver.model.pk, substitutions)
|
||||||
receiver.substitute(substitutions)
|
receiver.substitute(substitutions)
|
||||||
item.save(update_fields=['time_update'])
|
receiver.model.save(update_fields=['time_update'])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status=c.HTTP_200_OK,
|
status=c.HTTP_200_OK,
|
||||||
data=s.RSFormParseSerializer(item).data
|
data=s.RSFormParseSerializer(receiver.model).data
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
Django==5.2.7
|
Django==5.2.4
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.16.0
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==4.7.0
|
||||||
django-filter==25.2
|
django-filter==25.1
|
||||||
drf-spectacular==0.28.0
|
drf-spectacular==0.28.0
|
||||||
drf-spectacular-sidecar==2025.10.1
|
drf-spectacular-sidecar==2025.7.1
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
django-rest-passwordreset==1.5.0
|
django-rest-passwordreset==1.5.0
|
||||||
cctext==0.1.4
|
cctext==0.1.4
|
||||||
pyconcept==0.1.12
|
pyconcept==0.1.12
|
||||||
|
|
||||||
psycopg2-binary==2.9.11
|
psycopg2-binary==2.9.10
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
|
||||||
djangorestframework-stubs==3.16.5
|
djangorestframework-stubs==3.16.0
|
||||||
django-extensions==4.1
|
django-extensions==4.1
|
||||||
django-stubs==5.2.7
|
django-stubs==5.2.1
|
||||||
mypy==1.18.2
|
mypy==1.15.0
|
||||||
pylint==4.0.2
|
pylint==3.3.7
|
||||||
coverage==7.11.0
|
coverage==7.9.2
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
tzdata==2025.2
|
tzdata==2025.2
|
||||||
Django==5.2.7
|
Django==5.2.4
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.16.0
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==4.7.0
|
||||||
django-filter==25.2
|
django-filter==25.1
|
||||||
drf-spectacular==0.28.0
|
drf-spectacular==0.28.0
|
||||||
drf-spectacular-sidecar==2025.10.1
|
drf-spectacular-sidecar==2025.7.1
|
||||||
coreapi==2.3.3
|
coreapi==2.3.3
|
||||||
django-rest-passwordreset==1.5.0
|
django-rest-passwordreset==1.5.0
|
||||||
cctext==0.1.4
|
cctext==0.1.4
|
||||||
pyconcept==0.1.12
|
pyconcept==0.1.12
|
||||||
|
|
||||||
psycopg2-binary==2.9.11
|
psycopg2-binary==2.9.10
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
|
@ -150,16 +150,8 @@ def typificationInvalidStr():
|
||||||
return 'Invalid typification string'
|
return 'Invalid typification string'
|
||||||
|
|
||||||
|
|
||||||
def missingAttribution():
|
def invalidAssociation():
|
||||||
return f'Атрибутирование не найдено'
|
return f'Ассоциация не найдена'
|
||||||
|
|
||||||
|
|
||||||
def deleteInheritedAttribution():
|
|
||||||
return f'Попытка удалить наследованное атрибутирование'
|
|
||||||
|
|
||||||
|
|
||||||
def createdInheritedAttribution():
|
|
||||||
return f'Попытка установить атрибутирование между наследниками из одной КС'
|
|
||||||
|
|
||||||
|
|
||||||
def exteorFileVersionNotSupported():
|
def exteorFileVersionNotSupported():
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
"declaration-block-no-duplicate-properties": true,
|
"declaration-block-no-duplicate-properties": true,
|
||||||
"no-duplicate-selectors": true,
|
"no-duplicate-selectors": true,
|
||||||
"no-empty-source": true,
|
"no-empty-source": true,
|
||||||
"no-invalid-position-declaration": [true, { "ignoreAtRules": ["starting-style"] }],
|
|
||||||
|
|
||||||
"import-notation": null,
|
"import-notation": null,
|
||||||
"at-rule-empty-line-before": null,
|
"at-rule-empty-line-before": null,
|
||||||
|
|
@ -18,7 +17,6 @@
|
||||||
"at-rule-no-unknown": null,
|
"at-rule-no-unknown": null,
|
||||||
"comment-no-empty": null,
|
"comment-no-empty": null,
|
||||||
"comment-empty-line-before": null,
|
"comment-empty-line-before": null,
|
||||||
"custom-property-empty-line-before": null,
|
"custom-property-empty-line-before": null
|
||||||
"nesting-selector-no-missing-scoping-root": null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import globals from 'globals';
|
||||||
import typescriptPlugin from 'typescript-eslint';
|
import typescriptPlugin from 'typescript-eslint';
|
||||||
import typescriptParser from '@typescript-eslint/parser';
|
import typescriptParser from '@typescript-eslint/parser';
|
||||||
import reactPlugin from 'eslint-plugin-react';
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
|
import reactCompilerPlugin from 'eslint-plugin-react-compiler';
|
||||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
import importPlugin from 'eslint-plugin-import';
|
import importPlugin from 'eslint-plugin-import';
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
|
|
@ -35,7 +36,6 @@ const basicRules = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
reactHooksPlugin.configs.flat.recommended,
|
|
||||||
...typescriptPlugin.configs.recommendedTypeChecked,
|
...typescriptPlugin.configs.recommendedTypeChecked,
|
||||||
...typescriptPlugin.configs.stylisticTypeChecked,
|
...typescriptPlugin.configs.stylisticTypeChecked,
|
||||||
{
|
{
|
||||||
|
|
@ -65,6 +65,7 @@ export default [
|
||||||
|
|
||||||
plugins: {
|
plugins: {
|
||||||
'react': reactPlugin,
|
'react': reactPlugin,
|
||||||
|
'react-compiler': reactCompilerPlugin,
|
||||||
'react-hooks': reactHooksPlugin,
|
'react-hooks': reactHooksPlugin,
|
||||||
'simple-import-sort': simpleImportSort,
|
'simple-import-sort': simpleImportSort,
|
||||||
'import': importPlugin
|
'import': importPlugin
|
||||||
|
|
@ -72,6 +73,8 @@ export default [
|
||||||
settings: { react: { version: 'detect' } },
|
settings: { react: { version: 'detect' } },
|
||||||
rules: {
|
rules: {
|
||||||
...basicRules,
|
...basicRules,
|
||||||
|
'react-compiler/react-compiler': 'error',
|
||||||
|
'react-refresh/only-export-components': ['off', { allowConstantExport: true }],
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
|
|
||||||
1623
rsconcept/frontend/package-lock.json
generated
1623
rsconcept/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -10,36 +10,35 @@
|
||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "stylelint \"src/**/*.css\" && eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
|
"lintFix": "eslint . --report-unused-disable-directives --max-warnings 1 --fix",
|
||||||
"preview": "vite preview --port 3000"
|
"preview": "vite preview --port 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.8",
|
"@dagrejs/dagre": "^1.1.5",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@lezer/lr": "^1.4.3",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@uiw/codemirror-themes": "^4.25.3",
|
"@uiw/codemirror-themes": "^4.25.2",
|
||||||
"@uiw/react-codemirror": "^4.25.3",
|
"@uiw/react-codemirror": "^4.25.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.12.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"dompurify": "^3.3.0",
|
|
||||||
"global": "^4.4.0",
|
"global": "^4.4.0",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.545.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.65.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-intl": "^7.1.14",
|
"react-intl": "^7.1.14",
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.4",
|
||||||
"react-scan": "^0.4.3",
|
"react-scan": "^0.4.3",
|
||||||
"react-tabs": "^6.1.0",
|
"react-tabs": "^6.1.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
|
|
@ -47,40 +46,41 @@
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tw-animate-css": "1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"use-debounce": "^10.0.6",
|
"use-debounce": "^10.0.6",
|
||||||
"zod": "^4.1.12",
|
"zod": "^4.1.12",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.8.0",
|
"@lezer/generator": "^1.8.0",
|
||||||
"@playwright/test": "^1.56.1",
|
"@playwright/test": "^1.56.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.7.2",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
"@typescript-eslint/eslint-plugin": "^8.0.1",
|
||||||
"@typescript-eslint/parser": "^8.0.1",
|
"@typescript-eslint/parser": "^8.0.1",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-playwright": "^2.3.0",
|
"eslint-plugin-playwright": "^2.2.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"stylelint": "^16.25.0",
|
"stylelint": "^16.25.0",
|
||||||
"stylelint-config-recommended": "^17.0.0",
|
"stylelint-config-recommended": "^16.0.0",
|
||||||
"stylelint-config-standard": "^39.0.1",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-tailwindcss": "^1.0.0",
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.7",
|
"tailwindcss": "^4.0.7",
|
||||||
"ts-jest": "^29.4.5",
|
"ts-jest": "^29.4.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.46.3",
|
"typescript-eslint": "^8.46.0",
|
||||||
"vite": "^7.2.2"
|
"vite": "^7.1.9"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "ts-jest",
|
"preset": "ts-jest",
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ export function ApplicationLayout() {
|
||||||
|
|
||||||
<Navigation />
|
<Navigation />
|
||||||
|
|
||||||
<div className='overflow-x-auto max-w-dvw' style={{ maxHeight: viewportHeight }} inert={activeDialog !== null}>
|
<div
|
||||||
|
className='overflow-x-auto max-w-[100dvw]'
|
||||||
|
style={{ maxHeight: viewportHeight }}
|
||||||
|
inert={activeDialog !== null}
|
||||||
|
>
|
||||||
<main className='cc-scroll-y overflow-y-auto' style={{ minHeight: mainHeight }}>
|
<main className='cc-scroll-y overflow-y-auto' style={{ minHeight: mainHeight }}>
|
||||||
<GlobalLoader />
|
<GlobalLoader />
|
||||||
<MutationErrors />
|
<MutationErrors />
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,17 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useNavigate, useRouteError } from 'react-router';
|
import { useNavigate, useRouteError } from 'react-router';
|
||||||
|
|
||||||
import { Button } from '@/components/control';
|
import { Button } from '@/components/control';
|
||||||
import { InfoError } from '@/components/info-error';
|
import { InfoError } from '@/components/info-error';
|
||||||
import { isStaleBundleError } from '@/utils/utils';
|
|
||||||
|
|
||||||
export function ErrorFallback() {
|
export function ErrorFallback() {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isStaleBundleError(error)) {
|
|
||||||
console.warn('Detected stale bundle — reloading...');
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}, [error]);
|
|
||||||
|
|
||||||
function resetErrorBoundary() {
|
function resetErrorBoundary() {
|
||||||
Promise.resolve(router('/')).catch(console.error);
|
Promise.resolve(router('/')).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStaleBundleError(error)) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
|
|
||||||
<h1 className='my-2'>Обновление страницы...</h1>
|
|
||||||
<p>Обнаружена устаревшая версия приложения. Перезагрузка страницы...</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
|
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
|
||||||
<h1 className='my-2'>Что-то пошло не так!</h1>
|
<h1 className='my-2'>Что-то пошло не так!</h1>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function GlobalProviders({ children }: React.PropsWithChildren) {
|
||||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|
||||||
{import.meta.env.DEV ? <ReactQueryDevtools initialIsOpen={false} /> : null}
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const GlobalTooltips = () => {
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
place='bottom-start'
|
place='bottom-start'
|
||||||
offset={24}
|
offset={24}
|
||||||
className='max-w-80 wrap-break-word rounded-lg! select-none'
|
className='max-w-80 break-words rounded-lg! select-none'
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
float
|
float
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function Logo() {
|
||||||
<img
|
<img
|
||||||
alt=''
|
alt=''
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className='max-h-7 w-fit max-w-46 cursor-pointer'
|
className='max-h-7 w-fit max-w-46'
|
||||||
src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
src={size.isSmall ? '/logo_sign.svg' : !darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function NavigationButton({ icon, title, hideTitle, className, style, onC
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{icon ? icon : null}
|
{icon ? icon : null}
|
||||||
{text ? <span className='hidden lg:inline'>{text}</span> : null}
|
{text ? <span className='hidden md:inline'>{text}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useAIStore } from '@/features/ai/stores/ai-context';
|
|
||||||
|
|
||||||
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
|
||||||
import { useWindowSize } from '@/hooks/use-window-size';
|
import { useWindowSize } from '@/hooks/use-window-size';
|
||||||
import { useAppLayoutStore } from '@/stores/app-layout';
|
import { useAppLayoutStore } from '@/stores/app-layout';
|
||||||
|
|
@ -16,7 +14,6 @@ import { MenuAI } from './menu-ai';
|
||||||
import { MenuUser } from './menu-user';
|
import { MenuUser } from './menu-user';
|
||||||
import { NavigationButton } from './navigation-button';
|
import { NavigationButton } from './navigation-button';
|
||||||
import { useConceptNavigation } from './navigation-context';
|
import { useConceptNavigation } from './navigation-context';
|
||||||
import { SchemaTitle } from './schema-title';
|
|
||||||
import { ToggleNavigation } from './toggle-navigation';
|
import { ToggleNavigation } from './toggle-navigation';
|
||||||
|
|
||||||
export function Navigation() {
|
export function Navigation() {
|
||||||
|
|
@ -25,10 +22,6 @@ export function Navigation() {
|
||||||
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
|
||||||
const activeDialog = useDialogsStore(state => state.active);
|
const activeDialog = useDialogsStore(state => state.active);
|
||||||
|
|
||||||
const currentSchema = useAIStore(state => state.currentSchema);
|
|
||||||
const currentOSS = useAIStore(state => state.currentOSS);
|
|
||||||
const schemaTitle = currentSchema?.title || currentOSS?.title;
|
|
||||||
|
|
||||||
const navigateHome = (event: React.MouseEvent<Element>) =>
|
const navigateHome = (event: React.MouseEvent<Element>) =>
|
||||||
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
|
push({ path: urls.home, newTab: event.ctrlKey || event.metaKey });
|
||||||
const navigateLibrary = (event: React.MouseEvent<Element>) =>
|
const navigateLibrary = (event: React.MouseEvent<Element>) =>
|
||||||
|
|
@ -43,16 +36,15 @@ export function Navigation() {
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'pl-2 sm:pr-4 h-12 flex gap-2 justify-between cc-shadow-border',
|
'pl-2 sm:pr-4 h-12 flex cc-shadow-border',
|
||||||
'transition-[max-height,translate] ease-bezier duration-move',
|
'transition-[max-height,translate] ease-bezier duration-move',
|
||||||
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex items-center shrink-0' onClick={!size.isSmall ? navigateHome : undefined}>
|
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</div>
|
</div>
|
||||||
{schemaTitle ? <SchemaTitle isRSForm={!!currentSchema} title={schemaTitle} /> : null}
|
<div className='flex gap-2 items-center pr-2'>
|
||||||
<div className='flex gap-2 items-center pr-2 shrink-0'>
|
|
||||||
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
||||||
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
||||||
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { IconOSS, IconRSForm } from '@/components/icons';
|
|
||||||
import { globalIDs } from '@/utils/constants';
|
|
||||||
|
|
||||||
interface SchemaTitleProps {
|
|
||||||
isRSForm: boolean;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SchemaTitle({ isRSForm, title }: SchemaTitleProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
tabIndex={-1}
|
|
||||||
className={clsx(
|
|
||||||
'min-w-0 overflow-hidden max-w-fit',
|
|
||||||
'flex flex-1 items-center gap-2',
|
|
||||||
'text-md text-muted-foreground pointer-events-auto'
|
|
||||||
)}
|
|
||||||
aria-label='Название схемы'
|
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
|
||||||
data-tooltip-content={title}
|
|
||||||
>
|
|
||||||
{isRSForm ? <IconRSForm size='1.5rem' /> : <IconOSS size='1.5rem' />}
|
|
||||||
<span className='pt-0.5 font-medium truncate'>{title}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -112,7 +112,7 @@ function parseOssURL(id: string | undefined) {
|
||||||
|
|
||||||
function fallbackLoader() {
|
function fallbackLoader() {
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center items-center h-dvh'>
|
<div className='flex justify-center items-center h-[100dvh]'>
|
||||||
<Loader scale={6} />
|
<Loader scale={6} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,6 @@ axiosInstance.interceptors.request.use(config => {
|
||||||
.split('; ')
|
.split('; ')
|
||||||
.find(row => row.startsWith('csrftoken='))
|
.find(row => row.startsWith('csrftoken='))
|
||||||
?.split('=')[1];
|
?.split('=')[1];
|
||||||
|
|
||||||
if (!token && config.method !== 'get') {
|
|
||||||
console.warn('CSRF token not found for non-GET request');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers['x-csrftoken'] = token;
|
config.headers['x-csrftoken'] = token;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function ExportDropdown<T extends object = object>({
|
||||||
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
const { elementRef: ref, isOpen, toggle, handleBlur, hide } = useDropdown();
|
||||||
|
|
||||||
function handleExport(format: 'csv' | 'json') {
|
function handleExport(format: 'csv' | 'json') {
|
||||||
if (!data?.length) {
|
if (!data || data.length === 0) {
|
||||||
toast.error(infoMsg.noDataToExport);
|
toast.error(infoMsg.noDataToExport);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ export function useDataTable<TData extends RowData>({
|
||||||
pageSize: paginationPerPage
|
pageSize: paginationPerPage
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/incompatible-library
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
state: {
|
state: {
|
||||||
pagination: pagination,
|
pagination: pagination,
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export function DiagramFlow({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={cn('relative cc-mask-sides max-w-480 w-dvw', spaceMode && 'space-mode', className)}
|
className={cn('relative cc-mask-sides max-w-480 w-[100dvw]', spaceMode && 'space-mode', className)}
|
||||||
style={{ ...style, height: height }}
|
style={{ ...style, height: height }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
import { type AxiosError, isAxiosError } from '@/backend/api-transport';
|
import { type AxiosError, isAxiosError } from '@/backend/api-transport';
|
||||||
|
|
@ -19,17 +18,11 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
||||||
} else if (typeof error === 'string') {
|
} else if (typeof error === 'string') {
|
||||||
return <p>{error}</p>;
|
return <p>{error}</p>;
|
||||||
} else if (error instanceof ZodError) {
|
} else if (error instanceof ZodError) {
|
||||||
let errorData: unknown;
|
|
||||||
try {
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-base-to-string */
|
|
||||||
errorData = JSON.parse(error.toString());
|
|
||||||
} catch {
|
|
||||||
errorData = { message: error.message, issues: error.issues };
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Ошибка валидации данных</p>
|
<p>Ошибка валидации данных</p>
|
||||||
<PrettyJson data={errorData} />
|
{/* eslint-disable-next-line @typescript-eslint/no-base-to-string */}
|
||||||
|
<PrettyJson data={JSON.parse(error.toString()) as unknown} />;
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!isAxiosError(error)) {
|
} else if (!isAxiosError(error)) {
|
||||||
|
|
@ -41,7 +34,7 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
||||||
<p>
|
<p>
|
||||||
<b>Message:</b> {error.message}
|
<b>Message:</b> {error.message}
|
||||||
</p>
|
</p>
|
||||||
{error.stack && <pre className='whitespace-pre-wrap p-2 overflow-x-auto wrap-break-word'>{error.stack}</pre>}
|
{error.stack && <pre className='whitespace-pre-wrap p-2 overflow-x-auto break-words'>{error.stack}</pre>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -67,14 +60,6 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const isHtml = isResponseHtml(error.response);
|
const isHtml = isResponseHtml(error.response);
|
||||||
let sanitizedHtml: string | null = null;
|
|
||||||
if (isHtml) {
|
|
||||||
sanitizedHtml = DOMPurify.sanitize(error.response.data as string, {
|
|
||||||
USE_PROFILES: { html: true },
|
|
||||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'ul', 'li', 'br'],
|
|
||||||
ALLOWED_ATTR: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className='underline'>Ошибка</p>
|
<p className='underline'>Ошибка</p>
|
||||||
|
|
@ -82,11 +67,8 @@ export function DescribeError({ error }: { error: ErrorData }) {
|
||||||
{error.response.data && (
|
{error.response.data && (
|
||||||
<>
|
<>
|
||||||
<p className='mt-2 underline'>Описание</p>
|
<p className='mt-2 underline'>Описание</p>
|
||||||
{isHtml && sanitizedHtml ? (
|
{isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
|
||||||
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
|
{!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
|
||||||
) : (
|
|
||||||
<PrettyJson data={error.response.data as object} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -69,10 +71,9 @@ export function ComboMulti<Option>({
|
||||||
} else {
|
} else {
|
||||||
if ('onAdd' in restProps && typeof restProps.onAdd === 'function') {
|
if ('onAdd' in restProps && typeof restProps.onAdd === 'function') {
|
||||||
restProps.onAdd(newValue);
|
restProps.onAdd(newValue);
|
||||||
} else if ('onChange' in restProps && typeof restProps.onChange === 'function') {
|
|
||||||
restProps.onChange([...value, newValue]);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('onChange is not defined');
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange([...value, newValue]);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
@ -81,10 +82,9 @@ export function ComboMulti<Option>({
|
||||||
function handleRemoveValue(delValue: Option) {
|
function handleRemoveValue(delValue: Option) {
|
||||||
if ('onRemove' in restProps && typeof restProps.onRemove === 'function') {
|
if ('onRemove' in restProps && typeof restProps.onRemove === 'function') {
|
||||||
restProps.onRemove(delValue);
|
restProps.onRemove(delValue);
|
||||||
} else if ('onChange' in restProps && typeof restProps.onChange === 'function') {
|
|
||||||
restProps.onChange(value.filter(v => v !== delValue));
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('onChange is not defined');
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange(value.filter(v => v !== delValue));
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
@ -93,10 +93,9 @@ export function ComboMulti<Option>({
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if ('onClear' in restProps && typeof restProps.onClear === 'function') {
|
if ('onClear' in restProps && typeof restProps.onClear === 'function') {
|
||||||
restProps.onClear();
|
restProps.onClear();
|
||||||
} else if ('onChange' in restProps && typeof restProps.onChange === 'function') {
|
|
||||||
restProps.onChange([]);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('onChange is not defined');
|
assert('onChange' in restProps);
|
||||||
|
restProps.onChange([]);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { type HelpTopic } from '@/features/help';
|
import { type HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components/badge-help';
|
import { BadgeHelp } from '@/features/help/components/badge-help';
|
||||||
|
|
||||||
import { useEscapeKey } from '@/hooks/use-escape-key';
|
import { useEscapeKey } from '@/hooks/use-escape-key';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { globalIDs } from '@/utils/constants';
|
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { Button, MiniButton, SubmitButton } from '../control';
|
import { Button, MiniButton, SubmitButton } from '../control';
|
||||||
import { IconAlert, IconClose } from '../icons';
|
import { IconClose } from '../icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
import { cn } from '../utils';
|
import { cn } from '../utils';
|
||||||
|
|
||||||
|
|
@ -36,7 +33,7 @@ interface ModalFormProps extends ModalProps {
|
||||||
submitText?: string;
|
submitText?: string;
|
||||||
|
|
||||||
/** Tooltip for the submit button when the form is invalid. */
|
/** Tooltip for the submit button when the form is invalid. */
|
||||||
validationHint?: string;
|
submitInvalidTooltip?: string;
|
||||||
|
|
||||||
/** Indicates that submit button is enabled. */
|
/** Indicates that submit button is enabled. */
|
||||||
canSubmit?: boolean;
|
canSubmit?: boolean;
|
||||||
|
|
@ -63,7 +60,7 @@ export function ModalForm({
|
||||||
|
|
||||||
canSubmit = true,
|
canSubmit = true,
|
||||||
submitText = 'Продолжить',
|
submitText = 'Продолжить',
|
||||||
validationHint,
|
submitInvalidTooltip,
|
||||||
beforeSubmit,
|
beforeSubmit,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|
@ -124,7 +121,7 @@ export function ModalForm({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'@container/modal',
|
'@container/modal',
|
||||||
'max-h-[calc(100svh-8rem)] max-w-svw xs:max-w-[calc(100svw-2rem)]',
|
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||||
'overscroll-contain outline-hidden',
|
'overscroll-contain outline-hidden',
|
||||||
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||||
className
|
className
|
||||||
|
|
@ -134,24 +131,14 @@ export function ModalForm({
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={clsx('z-pop relative', 'my-2', 'flex justify-center', 'text-sm', !validationHint && 'gap-12')}>
|
<div className='z-pop my-2 flex gap-12 justify-center text-sm'>
|
||||||
<SubmitButton autoFocus text={submitText} className='min-w-28' disabled={!canSubmit} />
|
<SubmitButton
|
||||||
{validationHint ? (
|
autoFocus
|
||||||
<div
|
text={submitText}
|
||||||
className={clsx(
|
title={!canSubmit ? submitInvalidTooltip : ''}
|
||||||
'pt-0.5 w-12',
|
className='min-w-28'
|
||||||
'text-muted-foreground cc-animate-color duration-fade',
|
disabled={!canSubmit}
|
||||||
canSubmit ? 'hover:text-constructive' : 'hover:text-destructive'
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconAlert
|
|
||||||
size='1.5rem'
|
|
||||||
className='mx-auto'
|
|
||||||
data-tooltip-id={globalIDs.tooltip}
|
|
||||||
data-tooltip-html={validationHint}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<Button text='Отмена' aria-label='Закрыть' className='min-w-28' onClick={handleCancel} />
|
<Button text='Отмена' aria-label='Закрыть' className='min-w-28' onClick={handleCancel} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export function ModalView({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'@container/modal',
|
'@container/modal',
|
||||||
'max-w-svw xs:max-w-[calc(100svw-2rem)]',
|
'max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
|
||||||
'overscroll-contain outline-hidden',
|
'overscroll-contain outline-hidden',
|
||||||
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
overflowVisible ? 'overflow-visible' : 'overflow-auto',
|
||||||
fullScreen ? 'max-h-[calc(100svh-2rem)]' : 'max-h-[calc(100svh-8rem)]',
|
fullScreen ? 'max-h-[calc(100svh-2rem)]' : 'max-h-[calc(100svh-8rem)]',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const invalidVarMark = Decoration.mark({
|
||||||
});
|
});
|
||||||
|
|
||||||
const validMark = Decoration.mark({
|
const validMark = Decoration.mark({
|
||||||
class: 'text-accent-purple-foreground'
|
class: 'text-(--acc-fg-purple)'
|
||||||
});
|
});
|
||||||
|
|
||||||
class MarkVariablesPlugin {
|
class MarkVariablesPlugin {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function TabPromptEdit({ label, description, text, setText }: TabPromptEd
|
||||||
label='Текст шаблона'
|
label='Текст шаблона'
|
||||||
value={text}
|
value={text}
|
||||||
onChange={setText}
|
onChange={setText}
|
||||||
maxHeight='9.5rem'
|
maxHeight='10rem'
|
||||||
availableVariables={availableVariables}
|
availableVariables={availableVariables}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function DlgCreatePromptTemplate() {
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
validationHint={canSubmit ? '' : 'Введите уникальное название шаблона'}
|
submitInvalidTooltip='Введите уникальное название шаблона'
|
||||||
className='cc-column w-140 max-h-120 py-2 px-6'
|
className='cc-column w-140 max-h-120 py-2 px-6'
|
||||||
helpTopic={HelpTopic.ASSISTANT}
|
helpTopic={HelpTopic.ASSISTANT}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export function LoginPage() {
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
|
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
|
||||||
if (isAxiosError(error) && error.response?.status === 400) {
|
if (isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
return (
|
return (
|
||||||
<div className='text-sm select-text text-destructive'>
|
<div className='text-sm select-text text-destructive'>
|
||||||
На Портале отсутствует такое сочетание имени пользователя и пароля
|
На Портале отсутствует такое сочетание имени пользователя и пароля
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function Component() {
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
||||||
if (isAxiosError(error) && error.response?.status === 404) {
|
if (isAxiosError(error) && error.response && error.response.status === 404) {
|
||||||
return <div className='mx-auto mt-6 text-sm select-text text-destructive'>Данная ссылка не действительна</div>;
|
return <div className='mx-auto mt-6 text-sm select-text text-destructive'>Данная ссылка не действительна</div>;
|
||||||
}
|
}
|
||||||
return <InfoError error={error} />;
|
return <InfoError error={error} />;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export function Component() {
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
|
||||||
if (isAxiosError(error) && error.response?.status === 400) {
|
if (isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
return (
|
return (
|
||||||
<div className='mx-auto mt-6 text-sm select-text text-destructive'>Данный email не используется на Портале.</div>
|
<div className='mx-auto mt-6 text-sm select-text text-destructive'>Данный email не используется на Портале.</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,23 @@ export function HelpFormulaTree() {
|
||||||
|
|
||||||
<h2>Виды узлов</h2>
|
<h2>Виды узлов</h2>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
<span className='cc-sample-color bg-accent-green' />
|
<span className='cc-sample-color bg-(--acc-bg-green)' />
|
||||||
объявление идентификатора
|
объявление идентификатора
|
||||||
</p>
|
</p>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
<span className='cc-sample-color bg-accent-teal' />
|
<span className='cc-sample-color bg-(--acc-bg-teal)' />
|
||||||
глобальный идентификатор
|
глобальный идентификатор
|
||||||
</p>
|
</p>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
<span className='cc-sample-color bg-accent-orange' />
|
<span className='cc-sample-color bg-(--acc-bg-orange)' />
|
||||||
логическое выражение
|
логическое выражение
|
||||||
</p>
|
</p>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
<span className='cc-sample-color bg-accent-blue' />
|
<span className='cc-sample-color bg-(--acc-bg-blue)' />
|
||||||
типизированное выражение
|
типизированное выражение
|
||||||
</p>
|
</p>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
<span className='cc-sample-color bg-accent-red' />
|
<span className='cc-sample-color bg-(--acc-bg-red)' />
|
||||||
присвоение и итерация
|
присвоение и итерация
|
||||||
</p>
|
</p>
|
||||||
<p className='m-0'>
|
<p className='m-0'>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function HelpLibrary() {
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<span className='text-accent-green-foreground'>зеленым текстом</span> выделены ОСС
|
<span className='text-(--acc-fg-green)'>зеленым текстом</span> выделены ОСС
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<kbd>клик</kbd> по строке - переход к редактированию схемы
|
<kbd>клик</kbd> по строке - переход к редактированию схемы
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
import { urls, useConceptNavigation } from '@/app';
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
|
|
||||||
|
|
@ -11,14 +9,12 @@ export function HomePage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
|
|
||||||
useEffect(() => {
|
if (isAnonymous) {
|
||||||
// Note: Timeout is needed to let router initialize
|
// Note: Timeout is needed to let router initialize
|
||||||
const timeoutId = setTimeout(() => {
|
setTimeout(() => router.replace({ path: urls.login }), PARAMETER.minimalTimeout);
|
||||||
router.replace({ path: isAnonymous ? urls.login : urls.library });
|
} else {
|
||||||
}, PARAMETER.minimalTimeout);
|
setTimeout(() => router.replace({ path: urls.library }), PARAMETER.minimalTimeout);
|
||||||
|
}
|
||||||
return () => clearTimeout(timeoutId);
|
|
||||||
}, [router, isAnonymous]);
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,7 @@ export function DlgChangeLocation() {
|
||||||
overflowVisible
|
overflowVisible
|
||||||
header='Изменение расположения'
|
header='Изменение расположения'
|
||||||
submitText='Переместить'
|
submitText='Переместить'
|
||||||
validationHint={
|
submitInvalidTooltip={`Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.len_location}`}
|
||||||
isValid
|
|
||||||
? ''
|
|
||||||
: `Допустимы буквы, цифры, подчерк, пробел и "!". Сегмент пути не может начинаться и заканчиваться пробелом. Общая длина (с корнем) не должна превышать ${limits.len_location}`
|
|
||||||
}
|
|
||||||
canSubmit={isValid && isDirty}
|
canSubmit={isValid && isDirty}
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className='w-130 pb-3 px-6 h-36'
|
className='w-130 pb-3 px-6 h-36'
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { type ICreateVersionDTO, type IVersionInfo, schemaCreateVersion } from '../backend/types';
|
import { type ICreateVersionDTO, type IVersionInfo, schemaCreateVersion } from '../backend/types';
|
||||||
import { useCreateVersion } from '../backend/use-create-version';
|
import { useCreateVersion } from '../backend/use-create-version';
|
||||||
|
|
@ -41,15 +41,7 @@ export function DlgCreateVersion() {
|
||||||
mode: 'onChange'
|
mode: 'onChange'
|
||||||
});
|
});
|
||||||
const version = useWatch({ control, name: 'version' });
|
const version = useWatch({ control, name: 'version' });
|
||||||
const { canSubmit, hint } = (() => {
|
const canSubmit = !!version && !versions.find(ver => ver.version === version);
|
||||||
if (!version) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.versionEmpty };
|
|
||||||
} else if (versions.find(ver => ver.version === version)) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.versionTaken };
|
|
||||||
} else {
|
|
||||||
return { canSubmit: true, hint: '' };
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function onSubmit(data: ICreateVersionDTO) {
|
function onSubmit(data: ICreateVersionDTO) {
|
||||||
return versionCreate({ itemID, data }).then(onCreate);
|
return versionCreate({ itemID, data }).then(onCreate);
|
||||||
|
|
@ -60,7 +52,7 @@ export function DlgCreateVersion() {
|
||||||
header='Создание версии'
|
header='Создание версии'
|
||||||
className='cc-column w-120 py-2 px-6'
|
className='cc-column w-120 py-2 px-6'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={hint}
|
submitInvalidTooltip={errorMsg.versionTaken}
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { IconReset, IconSave } from '@/components/icons';
|
||||||
import { TextArea, TextInput } from '@/components/input';
|
import { TextArea, TextInput } from '@/components/input';
|
||||||
import { ModalView } from '@/components/modal';
|
import { ModalView } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
import { errorMsg } from '@/utils/labels';
|
||||||
|
|
||||||
import { type IUpdateVersionDTO, schemaUpdateVersion } from '../../backend/types';
|
import { type IUpdateVersionDTO, schemaUpdateVersion } from '../../backend/types';
|
||||||
import { useDeleteVersion } from '../../backend/use-delete-version';
|
import { useDeleteVersion } from '../../backend/use-delete-version';
|
||||||
|
|
@ -106,7 +106,7 @@ export function DlgEditVersions() {
|
||||||
<div className='cc-icons h-fit'>
|
<div className='cc-icons h-fit'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
type='submit'
|
type='submit'
|
||||||
title={isValid ? 'Сохранить изменения' : hintMsg.versionTaken}
|
title={isValid ? 'Сохранить изменения' : errorMsg.versionTaken}
|
||||||
aria-label='Сохранить изменения'
|
aria-label='Сохранить изменения'
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
disabled={!isDirty || !isValid || isProcessing}
|
disabled={!isDirty || !isValid || isProcessing}
|
||||||
|
|
|
||||||
|
|
@ -215,9 +215,9 @@ export const ossApi = {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
relocateConstituents: ({ itemID, data }: { itemID: number; data: IRelocateConstituentsDTO }) =>
|
relocateConstituents: (data: IRelocateConstituentsDTO) =>
|
||||||
axiosPost<IRelocateConstituentsDTO>({
|
axiosPost<IRelocateConstituentsDTO>({
|
||||||
endpoint: `/api/oss/${itemID}/relocate-constituents`,
|
endpoint: `/api/oss/relocate-constituents`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
successMessage: infoMsg.changesSaved
|
successMessage: infoMsg.changesSaved
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@ export const useRelocateConstituents = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
relocateConstituents: (data: { itemID: number; data: IRelocateConstituentsDTO }) => mutation.mutateAsync(data)
|
relocateConstituents: (data: IRelocateConstituentsDTO) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export function OperationTooltip() {
|
||||||
clickable
|
clickable
|
||||||
id={globalIDs.operation_tooltip}
|
id={globalIDs.operation_tooltip}
|
||||||
layer='z-topmost'
|
layer='z-topmost'
|
||||||
className='max-w-100 lg:max-w-140 dense max-h-80 lg:max-h-120! overflow-y-auto!'
|
className='max-w-140 dense max-h-120! overflow-y-auto!'
|
||||||
hidden={!hoverItem}
|
hidden={!hoverItem}
|
||||||
>
|
>
|
||||||
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem} /> : null}
|
{hoverItem && isOperationNode ? <InfoOperation operation={hoverItem} /> : null}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { HelpTopic } from '@/features/help';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type ICreateBlockDTO, type IOssLayout, schemaCreateBlock } from '../../backend/types';
|
import { type ICreateBlockDTO, type IOssLayout, schemaCreateBlock } from '../../backend/types';
|
||||||
import { useCreateBlock } from '../../backend/use-create-block';
|
import { useCreateBlock } from '../../backend/use-create-block';
|
||||||
|
|
@ -78,17 +77,7 @@ export function DlgCreateBlock() {
|
||||||
const children_blocks = useWatch({ control: methods.control, name: 'children_blocks' });
|
const children_blocks = useWatch({ control: methods.control, name: 'children_blocks' });
|
||||||
const children_operations = useWatch({ control: methods.control, name: 'children_operations' });
|
const children_operations = useWatch({ control: methods.control, name: 'children_operations' });
|
||||||
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.CARD);
|
||||||
const { canSubmit, hint } = (() => {
|
const canSubmit = methods.formState.isValid && !!title && !manager.oss.blocks.some(block => block.title === title);
|
||||||
if (!title) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.titleEmpty };
|
|
||||||
} else if (manager.oss.blocks.some(block => block.title === title)) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.blockTitleTaken };
|
|
||||||
} else if (!methods.formState.isValid) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.formInvalid };
|
|
||||||
} else {
|
|
||||||
return { canSubmit: true, hint: '' };
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function onSubmit(data: ICreateBlockDTO) {
|
function onSubmit(data: ICreateBlockDTO) {
|
||||||
data.position = manager.newBlockPosition(data);
|
data.position = manager.newBlockPosition(data);
|
||||||
|
|
@ -101,7 +90,6 @@ export function DlgCreateBlock() {
|
||||||
header='Создание блока'
|
header='Создание блока'
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={hint}
|
|
||||||
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
||||||
className='w-160 px-6 h-110'
|
className='w-160 px-6 h-110'
|
||||||
helpTopic={HelpTopic.CC_STRUCTURING}
|
helpTopic={HelpTopic.CC_STRUCTURING}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ export function TabBlockCard({ oss }: TabBlockCardProps) {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title' //
|
id='operation_title' //
|
||||||
label='Название'
|
label='Название'
|
||||||
placeholder='Введите название'
|
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { HelpTopic } from '@/features/help';
|
||||||
import { TextArea, TextInput } from '@/components/input';
|
import { TextArea, TextInput } from '@/components/input';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type ICreateSchemaDTO, type IOssLayout, schemaCreateSchema } from '../backend/types';
|
import { type ICreateSchemaDTO, type IOssLayout, schemaCreateSchema } from '../backend/types';
|
||||||
import { useCreateSchema } from '../backend/use-create-schema';
|
import { useCreateSchema } from '../backend/use-create-schema';
|
||||||
|
|
@ -65,17 +64,7 @@ export function DlgCreateSchema() {
|
||||||
mode: 'onChange'
|
mode: 'onChange'
|
||||||
});
|
});
|
||||||
const alias = useWatch({ control: control, name: 'item_data.alias' });
|
const alias = useWatch({ control: control, name: 'item_data.alias' });
|
||||||
const { canSubmit, hint } = (() => {
|
const canSubmit = isValid && !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
|
||||||
if (!alias) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.aliasEmpty };
|
|
||||||
} else if (manager.oss.operations.some(operation => operation.alias === alias)) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.schemaAliasTaken };
|
|
||||||
} else if (!isValid) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.formInvalid };
|
|
||||||
} else {
|
|
||||||
return { canSubmit: true, hint: '' };
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function onSubmit(data: ICreateSchemaDTO) {
|
function onSubmit(data: ICreateSchemaDTO) {
|
||||||
data.position = manager.newOperationPosition(data);
|
data.position = manager.newOperationPosition(data);
|
||||||
|
|
@ -88,7 +77,6 @@ export function DlgCreateSchema() {
|
||||||
header='Создание операции: Новая схема'
|
header='Создание операции: Новая схема'
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={hint}
|
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className='w-180 px-6 pb-3 cc-column'
|
className='w-180 px-6 pb-3 cc-column'
|
||||||
helpTopic={HelpTopic.CC_OSS}
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
|
|
@ -96,7 +84,6 @@ export function DlgCreateSchema() {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title' //
|
id='operation_title' //
|
||||||
label='Название'
|
label='Название'
|
||||||
placeholder='Введите название'
|
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
|
|
@ -106,7 +93,6 @@ export function DlgCreateSchema() {
|
||||||
id='operation_alias' //
|
id='operation_alias' //
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
className='w-80'
|
className='w-80'
|
||||||
placeholder='Введите сокращение'
|
|
||||||
{...register('item_data.alias')}
|
{...register('item_data.alias')}
|
||||||
error={errors.item_data?.alias}
|
error={errors.item_data?.alias}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { Loader } from '@/components/loader';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type ICreateSynthesisDTO, type IOssLayout, schemaCreateSynthesis } from '../../backend/types';
|
import { type ICreateSynthesisDTO, type IOssLayout, schemaCreateSynthesis } from '../../backend/types';
|
||||||
import { useCreateSynthesis } from '../../backend/use-create-synthesis';
|
import { useCreateSynthesis } from '../../backend/use-create-synthesis';
|
||||||
|
|
@ -74,17 +73,8 @@ export function DlgCreateSynthesis() {
|
||||||
});
|
});
|
||||||
const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
|
const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
|
||||||
const [activeTab, setActiveTab] = useState<TabID>(TabID.ARGUMENTS);
|
const [activeTab, setActiveTab] = useState<TabID>(TabID.ARGUMENTS);
|
||||||
const { canSubmit, hint } = (() => {
|
const canSubmit =
|
||||||
if (!alias) {
|
methods.formState.isValid && !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
|
||||||
return { canSubmit: false, hint: hintMsg.aliasEmpty };
|
|
||||||
} else if (manager.oss.operations.some(operation => operation.alias === alias)) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.schemaAliasTaken };
|
|
||||||
} else if (!methods.formState.isValid) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.formInvalid };
|
|
||||||
} else {
|
|
||||||
return { canSubmit: true, hint: '' };
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function onSubmit(data: ICreateSynthesisDTO) {
|
function onSubmit(data: ICreateSynthesisDTO) {
|
||||||
data.position = manager.newOperationPosition(data);
|
data.position = manager.newOperationPosition(data);
|
||||||
|
|
@ -97,7 +87,6 @@ export function DlgCreateSynthesis() {
|
||||||
header='Создание операции синтеза'
|
header='Создание операции синтеза'
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={hint}
|
|
||||||
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
||||||
className='w-180 px-6 h-128'
|
className='w-180 px-6 h-128'
|
||||||
helpTopic={HelpTopic.CC_OSS}
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ export function TabArguments({ oss }: TabArgumentsProps) {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Название'
|
label='Название'
|
||||||
placeholder='Введите название'
|
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
|
|
@ -41,7 +40,6 @@ export function TabArguments({ oss }: TabArgumentsProps) {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_alias' //
|
id='operation_alias' //
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
placeholder='Введите сокращение'
|
|
||||||
className='w-80'
|
className='w-80'
|
||||||
{...register('item_data.alias')}
|
{...register('item_data.alias')}
|
||||||
error={errors.item_data?.alias}
|
error={errors.item_data?.alias}
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,7 @@ export function TabSubstitutions({ oss }: TabSubstitutionsProps) {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea disabled value={validator.msg} rows={4} className={isCorrect ? '' : 'border-(--acc-fg-red) border-2'} />
|
||||||
disabled
|
|
||||||
value={validator.msg}
|
|
||||||
rows={4}
|
|
||||||
className={isCorrect ? '' : 'border-accent-red-foreground border-2'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ export function DlgEditBlock() {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title' //
|
id='operation_title' //
|
||||||
label='Название'
|
label='Название'
|
||||||
placeholder='Введите название'
|
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { Loader } from '@/components/loader';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type IOssLayout, type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types';
|
import { type IOssLayout, type IUpdateOperationDTO, OperationType, schemaUpdateOperation } from '../../backend/types';
|
||||||
import { useOssSuspense } from '../../backend/use-oss';
|
import { useOssSuspense } from '../../backend/use-oss';
|
||||||
|
|
@ -80,7 +79,6 @@ export function DlgEditOperation() {
|
||||||
header='Редактирование операции'
|
header='Редактирование операции'
|
||||||
submitText='Сохранить'
|
submitText='Сохранить'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={canSubmit ? '' : hintMsg.formInvalid}
|
|
||||||
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
|
||||||
className='w-160 px-6 h-128'
|
className='w-160 px-6 h-128'
|
||||||
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
|
helpTopic={HelpTopic.UI_SUBSTITUTIONS}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ export function TabOperation({ oss }: TabOperationProps) {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_title'
|
id='operation_title'
|
||||||
label='Название'
|
label='Название'
|
||||||
placeholder='Введите название'
|
|
||||||
{...register('item_data.title')}
|
{...register('item_data.title')}
|
||||||
error={errors.item_data?.title}
|
error={errors.item_data?.title}
|
||||||
/>
|
/>
|
||||||
|
|
@ -30,7 +29,6 @@ export function TabOperation({ oss }: TabOperationProps) {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='operation_alias' //
|
id='operation_alias' //
|
||||||
label='Сокращение'
|
label='Сокращение'
|
||||||
placeholder='Введите сокращение'
|
|
||||||
className='w-80'
|
className='w-80'
|
||||||
{...register('item_data.alias')}
|
{...register('item_data.alias')}
|
||||||
error={errors.item_data?.alias}
|
error={errors.item_data?.alias}
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,7 @@ export function TabSubstitutions({ oss }: TabSubstitutionsProps) {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextArea
|
<TextArea disabled value={validator.msg} rows={4} className={isCorrect ? '' : 'border-(--acc-fg-red) border-2'} />
|
||||||
disabled
|
|
||||||
value={validator.msg}
|
|
||||||
rows={4}
|
|
||||||
className={isCorrect ? '' : 'border-accent-red-foreground border-2'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { PickSchema } from '@/features/library/components/pick-schema';
|
||||||
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
import { Checkbox, TextArea, TextInput } from '@/components/input';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type IImportSchemaDTO, type IOssLayout, schemaImportSchema } from '../backend/types';
|
import { type IImportSchemaDTO, type IOssLayout, schemaImportSchema } from '../backend/types';
|
||||||
import { useImportSchema } from '../backend/use-import-schema';
|
import { useImportSchema } from '../backend/use-import-schema';
|
||||||
|
|
@ -74,17 +73,7 @@ export function DlgImportSchema() {
|
||||||
});
|
});
|
||||||
const alias = useWatch({ control: control, name: 'item_data.alias' });
|
const alias = useWatch({ control: control, name: 'item_data.alias' });
|
||||||
const clone_source = useWatch({ control: control, name: 'clone_source' });
|
const clone_source = useWatch({ control: control, name: 'clone_source' });
|
||||||
const { canSubmit, hint } = (() => {
|
const canSubmit = isValid && !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
|
||||||
if (!alias) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.aliasEmpty };
|
|
||||||
} else if (manager.oss.operations.some(operation => operation.alias === alias)) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.schemaAliasTaken };
|
|
||||||
} else if (!isValid) {
|
|
||||||
return { canSubmit: false, hint: hintMsg.formInvalid };
|
|
||||||
} else {
|
|
||||||
return { canSubmit: true, hint: '' };
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function onSubmit(data: IImportSchemaDTO) {
|
function onSubmit(data: IImportSchemaDTO) {
|
||||||
data.position = manager.newOperationPosition(data);
|
data.position = manager.newOperationPosition(data);
|
||||||
|
|
@ -112,7 +101,6 @@ export function DlgImportSchema() {
|
||||||
header='Создание операции: Загрузка'
|
header='Создание операции: Загрузка'
|
||||||
submitText='Создать'
|
submitText='Создать'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={hint}
|
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className='w-180 px-6 pb-3 cc-column'
|
className='w-180 px-6 pb-3 cc-column'
|
||||||
helpTopic={HelpTopic.CC_OSS}
|
helpTopic={HelpTopic.CC_OSS}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { MiniButton } from '@/components/control';
|
||||||
import { Loader } from '@/components/loader';
|
import { Loader } from '@/components/loader';
|
||||||
import { ModalForm } from '@/components/modal';
|
import { ModalForm } from '@/components/modal';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { hintMsg } from '@/utils/labels';
|
|
||||||
|
|
||||||
import { type IOssLayout, type IRelocateConstituentsDTO, schemaRelocateConstituents } from '../backend/types';
|
import { type IOssLayout, type IRelocateConstituentsDTO, schemaRelocateConstituents } from '../backend/types';
|
||||||
import { useOssSuspense } from '../backend/use-oss';
|
import { useOssSuspense } from '../backend/use-oss';
|
||||||
|
|
@ -106,13 +105,13 @@ export function DlgRelocateConstituents() {
|
||||||
function onSubmit(data: IRelocateConstituentsDTO) {
|
function onSubmit(data: IRelocateConstituentsDTO) {
|
||||||
data.items = moveTarget;
|
data.items = moveTarget;
|
||||||
if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
|
if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) {
|
||||||
return relocateConstituents({ itemID: oss.id, data: data });
|
return relocateConstituents(data);
|
||||||
} else {
|
} else {
|
||||||
return updatePositions({
|
return updatePositions({
|
||||||
isSilent: true,
|
isSilent: true,
|
||||||
itemID: oss.id,
|
itemID: oss.id,
|
||||||
data: layout
|
data: layout
|
||||||
}).then(() => relocateConstituents({ itemID: oss.id, data: data }));
|
}).then(() => relocateConstituents(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +120,7 @@ export function DlgRelocateConstituents() {
|
||||||
header='Перенос конституент'
|
header='Перенос конституент'
|
||||||
submitText='Переместить'
|
submitText='Переместить'
|
||||||
canSubmit={canSubmit}
|
canSubmit={canSubmit}
|
||||||
validationHint={canSubmit ? '' : hintMsg.relocateEmpty}
|
submitInvalidTooltip='Необходимо выбрать хотя бы одну собственную конституенту'
|
||||||
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
className='w-160 h-132 py-3 px-6'
|
className='w-160 h-132 py-3 px-6'
|
||||||
helpTopic={HelpTopic.UI_RELOCATE_CST}
|
helpTopic={HelpTopic.UI_RELOCATE_CST}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { useEffect, useState } from 'react';
|
||||||
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow';
|
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState } from 'reactflow';
|
||||||
|
|
||||||
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
import { type IConstituenta, type IRSForm } from '@/features/rsform';
|
||||||
import { FocusLabel } from '@/features/rsform/components/term-graph/focus-label';
|
|
||||||
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
|
import { TGEdgeTypes } from '@/features/rsform/components/term-graph/graph/tg-edge-types';
|
||||||
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
|
import { TGNodeTypes } from '@/features/rsform/components/term-graph/graph/tg-node-types';
|
||||||
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
|
import { SelectColoring } from '@/features/rsform/components/term-graph/select-coloring';
|
||||||
|
|
@ -101,10 +100,11 @@ export function TGReadonlyFlow({ schema }: TGReadonlyFlowProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative w-full h-full flex flex-col'>
|
<div className='relative w-full h-full flex flex-col'>
|
||||||
<div className='cc-tab-tools mt-2 flex flex-col items-center rounded-b-2xl backdrop-blur-xs'>
|
<div className='cc-tab-tools flex flex-col mt-2 items-center rounded-b-2xl backdrop-blur-xs'>
|
||||||
<ToolbarGraphFilter />
|
<ToolbarGraphFilter />
|
||||||
{focusCst ? <ToolbarFocusedCst resetFocus={() => setFocusCst(null)} /> : null}
|
{focusCst ? (
|
||||||
{focusCst ? <FocusLabel label={focusCst.alias} /> : null}
|
<ToolbarFocusedCst className='-translate-x-9' focus={focusCst} resetFocus={() => setFocusCst(null)} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
|
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
|
||||||
<SelectColoring className='rounded-b-none' schema={schema} />
|
<SelectColoring className='rounded-b-none' schema={schema} />
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,10 @@ export const rsformsApi = {
|
||||||
endpoint: version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`,
|
endpoint: version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`,
|
||||||
options: { responseType: 'blob' }
|
options: { responseType: 'blob' }
|
||||||
}),
|
}),
|
||||||
upload: ({ itemID, data }: { itemID: number; data: IRSFormUploadDTO }) =>
|
upload: (data: IRSFormUploadDTO) =>
|
||||||
axiosPatch<IRSFormUploadDTO, IRSFormDTO>({
|
axiosPatch<IRSFormUploadDTO, IRSFormDTO>({
|
||||||
schema: schemaRSForm,
|
schema: schemaRSForm,
|
||||||
endpoint: `/api/rsforms/${itemID}/load-trs`,
|
endpoint: `/api/rsforms/${data.itemID}/load-trs`,
|
||||||
request: {
|
request: {
|
||||||
data: data,
|
data: data,
|
||||||
successMessage: infoMsg.uploadSuccess
|
successMessage: infoMsg.uploadSuccess
|
||||||
|
|
|
||||||
|
|
@ -110,11 +110,11 @@ export class RSFormLoader {
|
||||||
parent.spawn_alias.push(cst.alias);
|
parent.spawn_alias.push(cst.alias);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.schema.attribution.forEach(attrib => {
|
this.schema.attribution.forEach(assoc => {
|
||||||
const container = this.cstByID.get(attrib.container)!;
|
const container = this.cstByID.get(assoc.container)!;
|
||||||
container.attributes.push(attrib.attribute);
|
container.attributes.push(assoc.attribute);
|
||||||
this.full_graph.addEdge(container.id, attrib.attribute);
|
this.full_graph.addEdge(container.id, assoc.attribute);
|
||||||
this.association_graph.addEdge(container.id, attrib.attribute);
|
this.association_graph.addEdge(container.id, assoc.attribute);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,10 @@ export type IRSFormDTO = z.infer<typeof schemaRSForm>;
|
||||||
|
|
||||||
/** Represents data, used for uploading {@link IRSForm} as file. */
|
/** Represents data, used for uploading {@link IRSForm} as file. */
|
||||||
export interface IRSFormUploadDTO {
|
export interface IRSFormUploadDTO {
|
||||||
|
itemID: number;
|
||||||
load_metadata: boolean;
|
load_metadata: boolean;
|
||||||
file: File;
|
file: File;
|
||||||
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Represents {@link IConstituenta} data, used in creation process. */
|
/** Represents {@link IConstituenta} data, used in creation process. */
|
||||||
|
|
@ -95,7 +97,7 @@ export type ISubstitutionsDTO = z.infer<typeof schemaSubstitutions>;
|
||||||
/** Represents data for creating or deleting an Attribution. */
|
/** Represents data for creating or deleting an Attribution. */
|
||||||
export type IAttribution = z.infer<typeof schemaAttribution>;
|
export type IAttribution = z.infer<typeof schemaAttribution>;
|
||||||
|
|
||||||
/** Represents data for clearing all attributions for a target constituenta. */
|
/** Represents data for clearing all associations for a target constituenta. */
|
||||||
export type IAttributionTargetDTO = z.infer<typeof schemaAttributionTarget>;
|
export type IAttributionTargetDTO = z.infer<typeof schemaAttributionTarget>;
|
||||||
|
|
||||||
/** Represents Constituenta list. */
|
/** Represents Constituenta list. */
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@ export const useUploadTRS = () => {
|
||||||
onError: () => client.invalidateQueries()
|
onError: () => client.invalidateQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
upload: (data: { itemID: number; data: IRSFormUploadDTO }) => mutation.mutateAsync(data)
|
upload: (data: IRSFormUploadDTO) => mutation.mutateAsync(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ interface InfoConstituentaProps extends React.ComponentProps<'div'> {
|
||||||
|
|
||||||
export function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) {
|
export function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('dense min-w-60 wrap-break-word', className)} {...restProps}>
|
<div className={cn('dense min-w-60 break-words', className)} {...restProps}>
|
||||||
<h2 className='cursor-default' title={data.is_inherited ? ' наследник' : undefined}>
|
<h2 className='cursor-default' title={data.is_inherited ? ' наследник' : undefined}>
|
||||||
{data.alias}
|
{data.alias}
|
||||||
{data.is_inherited ? <IconChild size='1rem' className='inline-icon align-middle ml-1 mt-1' /> : null}
|
{data.is_inherited ? <IconChild size='1rem' className='inline-icon align-middle ml-1 mt-1' /> : null}
|
||||||
|
|
|
||||||
|
|
@ -58,26 +58,18 @@ export function PickSubstitutions({
|
||||||
const [rightArgument, setRightArgument] = useState<ILibraryItem | null>(
|
const [rightArgument, setRightArgument] = useState<ILibraryItem | null>(
|
||||||
schemas.length === 1 && allowSelfSubstitution ? schemas[0] : null
|
schemas.length === 1 && allowSelfSubstitution ? schemas[0] : null
|
||||||
);
|
);
|
||||||
|
|
||||||
const [leftCst, setLeftCst] = useState<IConstituenta | null>(null);
|
|
||||||
const [rightCst, setRightCst] = useState<IConstituenta | null>(null);
|
|
||||||
|
|
||||||
const leftItems = !leftArgument
|
const leftItems = !leftArgument
|
||||||
? []
|
? []
|
||||||
: (leftArgument as IRSForm).items.filter(
|
: (leftArgument as IRSForm).items.filter(
|
||||||
cst =>
|
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
|
||||||
cst.id !== rightCst?.id && //
|
|
||||||
!value.find(item => item.original === cst.id) &&
|
|
||||||
(!filterCst || filterCst(cst))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [leftCst, setLeftCst] = useState<IConstituenta | null>(null);
|
||||||
|
const [rightCst, setRightCst] = useState<IConstituenta | null>(null);
|
||||||
const rightItems = !rightArgument
|
const rightItems = !rightArgument
|
||||||
? []
|
? []
|
||||||
: (rightArgument as IRSForm).items.filter(
|
: (rightArgument as IRSForm).items.filter(
|
||||||
cst =>
|
cst => !value.find(item => item.original === cst.id) && (!filterCst || filterCst(cst))
|
||||||
cst.id !== leftCst?.id && //
|
|
||||||
!value.find(item => item.original === cst.id) &&
|
|
||||||
(!filterCst || filterCst(cst))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [deleteRight, setDeleteRight] = useState(true);
|
const [deleteRight, setDeleteRight] = useState(true);
|
||||||
|
|
@ -86,12 +78,7 @@ export function PickSubstitutions({
|
||||||
const [ignores, setIgnores] = useState<ISubstituteConstituents[]>([]);
|
const [ignores, setIgnores] = useState<ISubstituteConstituents[]>([]);
|
||||||
const filteredSuggestions =
|
const filteredSuggestions =
|
||||||
suggestions?.filter(
|
suggestions?.filter(
|
||||||
item =>
|
item => !ignores.find(ignore => ignore.original === item.original && ignore.substitution === item.substitution)
|
||||||
!ignores.find(
|
|
||||||
ignore =>
|
|
||||||
(ignore.original === item.original && ignore.substitution === item.substitution) ||
|
|
||||||
(ignore.original === item.substitution && ignore.substitution === item.original)
|
|
||||||
)
|
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const substitutionData: IMultiSubstitution[] = [
|
const substitutionData: IMultiSubstitution[] = [
|
||||||
|
|
@ -206,7 +193,7 @@ export function PickSubstitutions({
|
||||||
size: 0,
|
size: 0,
|
||||||
cell: props =>
|
cell: props =>
|
||||||
props.row.original.is_suggestion ? (
|
props.row.original.is_suggestion ? (
|
||||||
<div className='flex max-w-fit'>
|
<div className='max-w-fit'>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Принять предложение'
|
title='Принять предложение'
|
||||||
icon={<IconAccept size='1rem' className='icon-green' />}
|
icon={<IconAccept size='1rem' className='icon-green' />}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { extractGlobals } from '../../models/rslang-api';
|
||||||
import { ccBracketMatching } from './bracket-matching';
|
import { ccBracketMatching } from './bracket-matching';
|
||||||
import { rsNavigation } from './click-navigation';
|
import { rsNavigation } from './click-navigation';
|
||||||
import { RSLanguage } from './rslang';
|
import { RSLanguage } from './rslang';
|
||||||
import { getLigatureSymbol, getSymbolSubstitute, isPotentialLigature, RSTextWrapper } from './text-editing';
|
import { getSymbolSubstitute, RSTextWrapper } from './text-editing';
|
||||||
import { rsHoverTooltip } from './tooltip';
|
import { rsHoverTooltip } from './tooltip';
|
||||||
|
|
||||||
const editorSetup: BasicSetupOptions = {
|
const editorSetup: BasicSetupOptions = {
|
||||||
|
|
@ -130,77 +130,56 @@ export const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
|
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
|
||||||
];
|
];
|
||||||
|
|
||||||
function handleAutoComplete(text: RSTextWrapper): boolean {
|
|
||||||
const selection = text.getSelection();
|
|
||||||
if (!selection.empty || !schema) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const wordRange = text.getWord(selection.from);
|
|
||||||
if (!wordRange) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const word = text.getText(wordRange.from, wordRange.to);
|
|
||||||
if (word.length > 2 && (word.startsWith('Pr') || word.startsWith('pr'))) {
|
|
||||||
text.setSelection(wordRange.from, wordRange.from + 2);
|
|
||||||
if (word.startsWith('Pr')) {
|
|
||||||
text.replaceWith('pr');
|
|
||||||
} else {
|
|
||||||
text.replaceWith('Pr');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const hint = text.getText(selection.from - 1, selection.from);
|
|
||||||
const type = guessCstType(hint);
|
|
||||||
if (hint === getCstTypePrefix(type)) {
|
|
||||||
text.setSelection(selection.from - 1, selection.from);
|
|
||||||
}
|
|
||||||
const takenAliases = [...extractGlobals(thisRef.current?.view?.state.doc.toString() ?? '')];
|
|
||||||
const newAlias = generateAlias(type, schema, takenAliases);
|
|
||||||
text.replaceWith(newAlias);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processInput(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
|
||||||
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
|
|
||||||
return handleAutoComplete(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.altKey) {
|
|
||||||
return text.processAltKey(event.code, event.shiftKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(event.ctrlKey || event.metaKey)) {
|
|
||||||
if (isPotentialLigature(event.key)) {
|
|
||||||
const selection = text.getSelection();
|
|
||||||
const prevSymbol = text.getText(selection.from - 1, selection.from);
|
|
||||||
const newSymbol = getLigatureSymbol(prevSymbol, event.key);
|
|
||||||
if (newSymbol) {
|
|
||||||
text.setSelection(selection.from - 1, selection.to);
|
|
||||||
text.replaceWith(newSymbol);
|
|
||||||
}
|
|
||||||
return !!newSymbol;
|
|
||||||
} else {
|
|
||||||
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
|
|
||||||
if (newSymbol) {
|
|
||||||
text.replaceWith(newSymbol);
|
|
||||||
}
|
|
||||||
return !!newSymbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.code === 'KeyQ' && onAnalyze) {
|
|
||||||
onAnalyze();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (!thisRef.current) {
|
if (!thisRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (processInput(event)) {
|
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
|
||||||
|
const selection = text.getSelection();
|
||||||
|
if (!selection.empty || !schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wordRange = text.getWord(selection.from);
|
||||||
|
if (wordRange) {
|
||||||
|
const word = text.getText(wordRange.from, wordRange.to);
|
||||||
|
if (word.length > 2 && (word.startsWith('Pr') || word.startsWith('pr'))) {
|
||||||
|
text.setSelection(wordRange.from, wordRange.from + 2);
|
||||||
|
if (word.startsWith('Pr')) {
|
||||||
|
text.replaceWith('pr');
|
||||||
|
} else {
|
||||||
|
text.replaceWith('Pr');
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hint = text.getText(selection.from - 1, selection.from);
|
||||||
|
const type = guessCstType(hint);
|
||||||
|
if (hint === getCstTypePrefix(type)) {
|
||||||
|
text.setSelection(selection.from - 1, selection.from);
|
||||||
|
}
|
||||||
|
const takenAliases = [...extractGlobals(thisRef.current.view?.state.doc.toString() ?? '')];
|
||||||
|
const newAlias = generateAlias(type, schema, takenAliases);
|
||||||
|
text.replaceWith(newAlias);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (event.altKey) {
|
||||||
|
if (text.processAltKey(event.code, event.shiftKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
} else if (!(event.ctrlKey || event.metaKey)) {
|
||||||
|
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
|
||||||
|
if (newSymbol) {
|
||||||
|
text.replaceWith(newSymbol);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
} else if (event.code === 'KeyQ' && onAnalyze) {
|
||||||
|
onAnalyze();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,6 @@ import { CodeMirrorWrapper } from '@/utils/codemirror';
|
||||||
|
|
||||||
import { TokenID } from '../../backend/types';
|
import { TokenID } from '../../backend/types';
|
||||||
|
|
||||||
/* Determines whether an input key is a potential ligature. */
|
|
||||||
export function isPotentialLigature(text: string): boolean {
|
|
||||||
return text == '=';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Determines symbol for ligature replacement. */
|
|
||||||
export function getLigatureSymbol(prevSymbol: string, text: string): string | undefined {
|
|
||||||
if (text == '=') {
|
|
||||||
// prettier-ignore
|
|
||||||
switch (prevSymbol) {
|
|
||||||
case '!': return '≠';
|
|
||||||
case '>': return '≥';
|
|
||||||
case '<': return '≤';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
if (shiftPressed) {
|
if (shiftPressed) {
|
||||||
|
|
@ -190,12 +172,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
|
||||||
this.replaceWith('∃');
|
this.replaceWith('∃');
|
||||||
return true;
|
return true;
|
||||||
case TokenID.SET_IN:
|
case TokenID.SET_IN:
|
||||||
if (this.getPrevSymbol() == '!') {
|
this.replaceWith('∈');
|
||||||
this.setSelection(selection.from - 1, selection.to);
|
|
||||||
this.replaceWith('∉');
|
|
||||||
} else {
|
|
||||||
this.replaceWith('∈');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case TokenID.SET_NOT_IN:
|
case TokenID.SET_NOT_IN:
|
||||||
this.replaceWith('∉');
|
this.replaceWith('∉');
|
||||||
|
|
@ -207,12 +184,7 @@ export class RSTextWrapper extends CodeMirrorWrapper {
|
||||||
this.replaceWith('&');
|
this.replaceWith('&');
|
||||||
return true;
|
return true;
|
||||||
case TokenID.SUBSET_OR_EQ:
|
case TokenID.SUBSET_OR_EQ:
|
||||||
if (this.getPrevSymbol() == '!') {
|
this.replaceWith('⊆');
|
||||||
this.setSelection(selection.from - 1, selection.to);
|
|
||||||
this.replaceWith('⊄');
|
|
||||||
} else {
|
|
||||||
this.replaceWith('⊆');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case TokenID.LOGIC_IMPLICATION:
|
case TokenID.LOGIC_IMPLICATION:
|
||||||
this.replaceWith('⇒');
|
this.replaceWith('⇒');
|
||||||
|
|
@ -236,23 +208,13 @@ export class RSTextWrapper extends CodeMirrorWrapper {
|
||||||
this.replaceWith('Z');
|
this.replaceWith('Z');
|
||||||
return true;
|
return true;
|
||||||
case TokenID.SUBSET:
|
case TokenID.SUBSET:
|
||||||
if (this.getPrevSymbol() == '!') {
|
this.replaceWith('⊂');
|
||||||
this.setSelection(selection.from - 1, selection.to);
|
|
||||||
this.replaceWith('⊄');
|
|
||||||
} else {
|
|
||||||
this.replaceWith('⊂');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case TokenID.NOT_SUBSET:
|
case TokenID.NOT_SUBSET:
|
||||||
this.replaceWith('⊄');
|
this.replaceWith('⊄');
|
||||||
return true;
|
return true;
|
||||||
case TokenID.EQUAL:
|
case TokenID.EQUAL:
|
||||||
if (this.getPrevSymbol() == '!') {
|
this.replaceWith('=');
|
||||||
this.setSelection(selection.from - 1, selection.to);
|
|
||||||
this.replaceWith('≠');
|
|
||||||
} else {
|
|
||||||
this.replaceWith('=');
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
case TokenID.NOTEQUAL:
|
case TokenID.NOTEQUAL:
|
||||||
this.replaceWith('≠');
|
this.replaceWith('≠');
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
interface FocusLabelProps {
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FocusLabel({ label }: FocusLabelProps) {
|
|
||||||
return (
|
|
||||||
<div className={clsx('px-1', 'select-none', 'hover:bg-background', 'text-accent-purple-foreground rounded-md')}>
|
|
||||||
<span aria-label='Фокус-конституента' className='whitespace-nowrap'>
|
|
||||||
Фокус
|
|
||||||
<b> {label} </b>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user